diff --git a/ElasticDatabaseTools.sln b/ElasticDatabaseTools.sln index 8b388cd..b6a9025 100644 --- a/ElasticDatabaseTools.sln +++ b/ElasticDatabaseTools.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29709.97 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.SqlDatabase.ElasticScale.Client", "Src\ElasticScale.Client\Microsoft.Azure.SqlDatabase.ElasticScale.Client.csproj", "{4C3B3EC4-5702-469E-800E-313FB27A0A2B}" EndProject @@ -11,7 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.SqlDatabase EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.SqlDatabase.ElasticScale.Query.UnitTests", "Test\ElasticScale.Query.UnitTests\Microsoft.Azure.SqlDatabase.ElasticScale.Query.UnitTests.csproj", "{74CEE77F-D2C7-4B8B-9411-8F97F4E803FA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElasticScaleStarterKit", "Samples\ElasticScaleStarterKit\ElasticScaleStarterKit.csproj", "{115A0283-AC42-4D37-97F2-106D168E04D2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElasticScaleStarterKit", "Samples\ElasticScaleStarterKit\ElasticScaleStarterKit.csproj", "{115A0283-AC42-4D37-97F2-106D168E04D2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkMultiTenant", "Samples\EFMultiTenant\EntityFrameworkMultiTenant.csproj", "{BC17F3EF-FEB4-4186-9342-E2D18F1E56AB}" EndProject @@ -21,6 +21,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElasticDapper", "Samples\Da EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShardSqlCmd", "Samples\ShardSqlCmd\ShardSqlCmd.csproj", "{B210D6E5-7171-4117-9C77-3F2CB59D04D8}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{F7A2F005-3B53-495D-B0EC-CE41FAB4DC75}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{9C78C56E-7738-4D3B-9722-67A4CB028A75}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{A0CBF252-A093-4448-BC10-1A1EF10DB64E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Cover|Any CPU = Cover|Any CPU @@ -143,6 +149,17 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {4C3B3EC4-5702-469E-800E-313FB27A0A2B} = {A0CBF252-A093-4448-BC10-1A1EF10DB64E} + {9336E9E7-19BF-49AC-92E3-19FA6B98921E} = {9C78C56E-7738-4D3B-9722-67A4CB028A75} + {BEA6F911-BA98-462C-99AF-3B0595DE2307} = {9C78C56E-7738-4D3B-9722-67A4CB028A75} + {74CEE77F-D2C7-4B8B-9411-8F97F4E803FA} = {9C78C56E-7738-4D3B-9722-67A4CB028A75} + {115A0283-AC42-4D37-97F2-106D168E04D2} = {F7A2F005-3B53-495D-B0EC-CE41FAB4DC75} + {BC17F3EF-FEB4-4186-9342-E2D18F1E56AB} = {F7A2F005-3B53-495D-B0EC-CE41FAB4DC75} + {8EB66613-D5A2-4683-B91A-6AF904FD6B70} = {F7A2F005-3B53-495D-B0EC-CE41FAB4DC75} + {AC76C04B-881E-4CB9-B491-4D19B68459F1} = {F7A2F005-3B53-495D-B0EC-CE41FAB4DC75} + {B210D6E5-7171-4117-9C77-3F2CB59D04D8} = {F7A2F005-3B53-495D-B0EC-CE41FAB4DC75} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {24237160-521B-4CD0-87AB-8994A5E50BE2} EndGlobalSection diff --git a/Samples/Dapper/DataClasses.cs b/Samples/Dapper/Blog.cs similarity index 86% rename from Samples/Dapper/DataClasses.cs rename to Samples/Dapper/Blog.cs index 6abf03e..1c3cae1 100644 --- a/Samples/Dapper/DataClasses.cs +++ b/Samples/Dapper/Blog.cs @@ -1,15 +1,15 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace ElasticDapper -{ - // Let's use the standard blogging class - public class Blog - { - public int BlogId { get; set; } - - public string Name { get; set; } - - public string Url { get; set; } - } -} +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace ElasticDapper +{ + // Let's use the standard blogging class + public class Blog + { + public long BlogId { get; set; } + + public string Name { get; set; } + + public string Url { get; set; } + } +} diff --git a/Samples/Dapper/ElasticDapper.csproj b/Samples/Dapper/ElasticDapper.csproj index d04f160..0ee4f8a 100644 --- a/Samples/Dapper/ElasticDapper.csproj +++ b/Samples/Dapper/ElasticDapper.csproj @@ -5,14 +5,13 @@ - - - - - + + + + \ No newline at end of file diff --git a/Samples/Dapper/LICENSE b/Samples/Dapper/LICENSE index b8b569d..06f7e30 100644 --- a/Samples/Dapper/LICENSE +++ b/Samples/Dapper/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Microsoft +Copyright (c) 2024 Microsoft Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Samples/Dapper/Program.cs b/Samples/Dapper/Program.cs index dafc787..051f663 100644 --- a/Samples/Dapper/Program.cs +++ b/Samples/Dapper/Program.cs @@ -3,10 +3,10 @@ using System; using System.Collections.Generic; -using System.Data.SqlClient; using Dapper; using DapperExtensions; using Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement; +using Microsoft.Data.SqlClient; //////////////////////////////////////////////////////////////////////////////////////// // This sample illustrates the adjustments that need to be made to use Dapper @@ -28,9 +28,8 @@ internal class Program private static string s_shardmapmgrdb = "[YourShardMapManagerDatabaseName]"; private static string s_shard1 = "[YourShard01DatabaseName]"; private static string s_shard2 = "[YourShard02DatabaseName]"; - private static string s_userName = "YourUserName"; - private static string s_password = "YourPassword"; private static string s_applicationName = "ESC_Dapv1.0"; + private static SqlAuthenticationMethod s_authenticationMethod = SqlAuthenticationMethod.ActiveDirectoryDefault; // Just two tenants for now. // Those we will allocate to shards. @@ -39,19 +38,14 @@ internal class Program public static void Main() { - SqlConnectionStringBuilder connStrBldr = new SqlConnectionStringBuilder - { - UserID = s_userName, - Password = s_password, - ApplicationName = s_applicationName - }; + SqlConnectionStringBuilder connStrBldr = GetConnectionStringBuilder(); // Bootstrap the shard map manager, register shards, and store mappings of tenants to shards // Note that you can keep working with existing shard maps. There is no need to // re-create and populate the shard map from scratch every time. Sharding shardingLayer = new Sharding(s_server, s_shardmapmgrdb, connStrBldr.ConnectionString); - shardingLayer.RegisterNewShard(s_server, s_shard1, connStrBldr.ConnectionString, s_tenantId1); - shardingLayer.RegisterNewShard(s_server, s_shard2, connStrBldr.ConnectionString, s_tenantId2); + shardingLayer.RegisterNewShard(s_server, s_shard1, s_tenantId1); + shardingLayer.RegisterNewShard(s_server, s_shard2, s_tenantId2); // Create schema on each shard. foreach (string shard in new[] {s_shard1, s_shard2}) @@ -67,89 +61,74 @@ public static void Main() Console.Write("Enter a name for a new Blog: "); var name = Console.ReadLine(); - SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() => + using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey( + key: s_tenantId1, + connectionString: connStrBldr.ConnectionString, + options: ConnectionOptions.Validate)) { - using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey( - key: s_tenantId1, - connectionString: connStrBldr.ConnectionString, - options: ConnectionOptions.Validate)) - { - var blog = new Blog { Name = name }; - sqlconn.Insert(blog); - } - }); + var blog = new Blog { Name = name }; + sqlconn.Insert(blog); + } - SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() => + using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey( + key: s_tenantId1, + connectionString: connStrBldr.ConnectionString, + options: ConnectionOptions.Validate)) { - using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey( - key: s_tenantId1, - connectionString: connStrBldr.ConnectionString, - options: ConnectionOptions.Validate)) + // Display all Blogs for tenant 1 + IEnumerable result = sqlconn.Query(@" + SELECT * + FROM Blog + ORDER BY Name"); + + Console.WriteLine("All blogs for tenant id {0}:", s_tenantId1); + foreach (var item in result) { - // Display all Blogs for tenant 1 - IEnumerable result = sqlconn.Query(@" - SELECT * - FROM Blog - ORDER BY Name"); - - Console.WriteLine("All blogs for tenant id {0}:", s_tenantId1); - foreach (var item in result) - { - Console.WriteLine(item.Name); - } + Console.WriteLine(item.Name); } - }); + } // Do work for tenant 2 :-) // Here I am going to illustrate how to integrate // with DapperExtensions which saves us the T-SQL // - SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() => + using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey( + key: s_tenantId2, + connectionString: connStrBldr.ConnectionString, + options: ConnectionOptions.Validate)) { - using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey( - key: s_tenantId2, - connectionString: connStrBldr.ConnectionString, - options: ConnectionOptions.Validate)) + // Display all Blogs for tenant 2 + IEnumerable result = sqlconn.GetList(); + Console.WriteLine("All blogs for tenant id {0}:", s_tenantId2); + foreach (var item in result) { - // Display all Blogs for tenant 2 - IEnumerable result = sqlconn.GetList(); - Console.WriteLine("All blogs for tenant id {0}:", s_tenantId2); - foreach (var item in result) - { - Console.WriteLine(item.Name); - } + Console.WriteLine(item.Name); } - }); + } // Create and save a new Blog Console.Write("Enter a name for a new Blog: "); var name2 = Console.ReadLine(); - SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() => + using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey( + key: s_tenantId2, + connectionString: connStrBldr.ConnectionString, + options: ConnectionOptions.Validate)) { - using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey( - key: s_tenantId2, - connectionString: connStrBldr.ConnectionString, - options: ConnectionOptions.Validate)) - { - var blog = new Blog { Name = name2 }; - sqlconn.Insert(blog); - } - }); + var blog = new Blog { Name = name2 }; + sqlconn.Insert(blog); + } - SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() => + using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey(s_tenantId2, connStrBldr.ConnectionString, ConnectionOptions.Validate)) { - using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey(s_tenantId2, connStrBldr.ConnectionString, ConnectionOptions.Validate)) + // Display all Blogs for tenant 2 + IEnumerable result = sqlconn.GetList(); + Console.WriteLine("All blogs for tenant id {0}:", s_tenantId2); + foreach (var item in result) { - // Display all Blogs for tenant 2 - IEnumerable result = sqlconn.GetList(); - Console.WriteLine("All blogs for tenant id {0}:", s_tenantId2); - foreach (var item in result) - { - Console.WriteLine(item.Name); - } + Console.WriteLine(item.Name); } - }); + } Console.WriteLine("Press any key to exit..."); Console.ReadKey(); @@ -157,14 +136,10 @@ FROM Blog private static void CreateSchema(string shardName) { - SqlConnectionStringBuilder connStrBldr = new SqlConnectionStringBuilder - { - UserID = s_userName, - Password = s_password, - ApplicationName = s_applicationName, - DataSource = s_server, - InitialCatalog = shardName - }; + SqlConnectionStringBuilder connStrBldr = GetConnectionStringBuilder(); + + connStrBldr.DataSource = s_server; + connStrBldr.InitialCatalog = shardName; using (SqlConnection conn = new SqlConnection(connStrBldr.ToString())) { @@ -178,5 +153,19 @@ [Url] [nvarchar](max) NULL, )"); } } + + private static SqlConnectionStringBuilder GetConnectionStringBuilder() + { + var connBuilder = new SqlConnectionStringBuilder + { + Authentication = s_authenticationMethod, + ApplicationName = s_applicationName, + CommandTimeout = 60, + ConnectTimeout = 60, + TrustServerCertificate = true + }; + + return connBuilder; + } } } diff --git a/Samples/Dapper/Sharding.cs b/Samples/Dapper/Sharding.cs index a7625cf..4e751ff 100644 --- a/Samples/Dapper/Sharding.cs +++ b/Samples/Dapper/Sharding.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Data.SqlClient; using Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement; +using Microsoft.Data.SqlClient; namespace ElasticDapper { @@ -25,41 +25,41 @@ public Sharding(string smmserver, string smmdatabase, string smmconnstr) ShardMapManager smm; if (!ShardMapManagerFactory.TryGetSqlShardMapManager(connStrBldr.ConnectionString, ShardMapManagerLoadPolicy.Lazy, out smm)) { - this.ShardMapManager = ShardMapManagerFactory.CreateSqlShardMapManager(connStrBldr.ConnectionString); + ShardMapManager = ShardMapManagerFactory.CreateSqlShardMapManager(connStrBldr.ConnectionString); } else { - this.ShardMapManager = smm; + ShardMapManager = smm; } ListShardMap sm; if (!ShardMapManager.TryGetListShardMap("ElasticScaleWithDapper", out sm)) { - this.ShardMap = ShardMapManager.CreateListShardMap("ElasticScaleWithDapper"); + ShardMap = ShardMapManager.CreateListShardMap("ElasticScaleWithDapper"); } else { - this.ShardMap = sm; + ShardMap = sm; } } // Enter a new shard - i.e. an empty database - to the shard map, allocate a first tenant to it - public void RegisterNewShard(string server, string database, string connstr, int key) + public void RegisterNewShard(string server, string database, int key) { Shard shard; ShardLocation shardLocation = new ShardLocation(server, database); - if (!this.ShardMap.TryGetShard(shardLocation, out shard)) + if (!ShardMap.TryGetShard(shardLocation, out shard)) { - shard = this.ShardMap.CreateShard(shardLocation); + shard = ShardMap.CreateShard(shardLocation); } // Register the mapping of the tenant to the shard in the shard map. // After this step, DDR on the shard map can be used PointMapping mapping; - if (!this.ShardMap.TryGetMappingForKey(key, out mapping)) + if (!ShardMap.TryGetMappingForKey(key, out mapping)) { - this.ShardMap.CreatePointMapping(key, shard); + ShardMap.CreatePointMapping(key, shard); } } } diff --git a/Samples/Dapper/SqlDatabaseUtils.cs b/Samples/Dapper/SqlDatabaseUtils.cs deleted file mode 100644 index 66de772..0000000 --- a/Samples/Dapper/SqlDatabaseUtils.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling; - -namespace ElasticDapper -{ - /// - /// Helper methods for interacting with SQL Databases. - /// - internal static class SqlDatabaseUtils - { - /// - /// Gets the retry policy to use for connections to SQL Server. - /// - public static RetryPolicy SqlRetryPolicy - { - get { return new RetryPolicy(10, TimeSpan.FromSeconds(5)); } - } - } -} diff --git a/Samples/ElasticScaleStarterKit/App.config b/Samples/ElasticScaleStarterKit/App.config new file mode 100644 index 0000000..a6b214c --- /dev/null +++ b/Samples/ElasticScaleStarterKit/App.config @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Samples/ElasticScaleStarterKit/Configuration.cs b/Samples/ElasticScaleStarterKit/Configuration.cs index 3d1fd33..0a46df9 100644 --- a/Samples/ElasticScaleStarterKit/Configuration.cs +++ b/Samples/ElasticScaleStarterKit/Configuration.cs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using System.Configuration; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; namespace ElasticScaleStarterKit { @@ -76,17 +77,21 @@ public static string GetCredentialsConnectionString() string userId = ConfigurationManager.AppSettings["UserName"] ?? string.Empty; string password = ConfigurationManager.AppSettings["Password"] ?? string.Empty; - // Get Integrated Security from the app.config file. - // If it exists, then parse it (throw exception on failure), otherwise default to false. - string integratedSecurityString = ConfigurationManager.AppSettings["IntegratedSecurity"]; - bool integratedSecurity = integratedSecurityString != null && bool.Parse(integratedSecurityString); + string trustServerCertificateString = ConfigurationManager.AppSettings["TrustServerCertificate"] ?? string.Empty; + + var trustServerCertificate = trustServerCertificateString != null && bool.Parse(trustServerCertificateString); + + // Get Sql Auth method from the app.config file. + SqlAuthenticationMethod authMethod; + var enumString = ConfigurationManager.AppSettings["SqlAuthenticationMethod"]; + if (!Enum.TryParse(enumString, out authMethod)) + { + throw new ArgumentException("Invalid SqlAuthenticationMethod in app.config"); + } SqlConnectionStringBuilder connStr = new SqlConnectionStringBuilder { - // DDR and MSQ require credentials to be set - UserID = userId, - Password = password, - IntegratedSecurity = integratedSecurity, + Authentication = authMethod, // DataSource and InitialCatalog cannot be set for DDR and MSQ APIs, because these APIs will // determine the DataSource and InitialCatalog for you. @@ -97,10 +102,52 @@ public static string GetCredentialsConnectionString() // // Other SqlClient ConnectionString keywords are supported. + TrustServerCertificate = trustServerCertificate, + ApplicationName = "ESC_SKv1.0", - ConnectTimeout = 30 + + // Set to 120 if ActiveDirectoryDeviceCodeFlow + // not even the fastest cut and pasters can get the device code + // into the browser and click through in 30 seconds. + ConnectTimeout = authMethod == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow ? 120 : 30, }; + + // DEVNOTE: NotSpecified behaves the same as SqlPassword (i.e. Sql Auth) + if (authMethod == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || + authMethod == SqlAuthenticationMethod.ActiveDirectoryMSI || + authMethod == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal || + authMethod == SqlAuthenticationMethod.ActiveDirectoryPassword || + authMethod == SqlAuthenticationMethod.SqlPassword || + authMethod == SqlAuthenticationMethod.NotSpecified) + { + // DDR and MSQ require credentials to be set + + // ActiveDirectoryManagedIdentity / ActiveDirectoryMSI when using a System Managed System Identify does not use a UserID + if (authMethod != SqlAuthenticationMethod.ActiveDirectoryManagedIdentity && + authMethod != SqlAuthenticationMethod.ActiveDirectoryMSI) + { + if (userId == string.Empty) + { + throw new ArgumentException("UserName must be specified in app.config"); + } + } + + connStr.UserID = userId; + + // ActiveDirectoryManagedIdentity/ActiveDirectoryMSI does not use a Password. + if (authMethod != SqlAuthenticationMethod.ActiveDirectoryManagedIdentity && + authMethod != SqlAuthenticationMethod.ActiveDirectoryMSI) + { + if (password == string.Empty) + { + throw new ArgumentException("Password must be specified in app.config"); + } + + connStr.Password = password; + } + } + return connStr.ToString(); } } -} +} \ No newline at end of file diff --git a/Samples/ElasticScaleStarterKit/DataDependentRoutingSample.cs b/Samples/ElasticScaleStarterKit/DataDependentRoutingSample.cs index 780b0f7..a34c397 100644 --- a/Samples/ElasticScaleStarterKit/DataDependentRoutingSample.cs +++ b/Samples/ElasticScaleStarterKit/DataDependentRoutingSample.cs @@ -2,9 +2,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Data.SqlClient; using System.Linq; using Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement; +using Microsoft.Data.SqlClient; namespace ElasticScaleStarterKit { @@ -64,21 +64,23 @@ private static void AddCustomer( // Open and execute the command with retry for transient faults. Note that if the command fails, the connection is closed, so // the entire block is wrapped in a retry. This means that only one command should be executed per block, since if we had multiple // commands then the first command may be executed multiple times if later commands fail. - SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() => + + // Looks up the key in the shard map and opens a connection to the shard + using (SqlConnection conn = shardMap.OpenConnectionForKey(customerId, credentialsConnectionString)) { - // Looks up the key in the shard map and opens a connection to the shard - using (SqlConnection conn = shardMap.OpenConnectionForKey(customerId, credentialsConnectionString)) + // Create a simple command that will insert or update the customer information + using (SqlCommand cmd = conn.CreateCommand()) { - // Create a simple command that will insert or update the customer information - SqlCommand cmd = conn.CreateCommand(); + cmd.RetryLogicProvider = SqlDatabaseUtils.SqlRetryProvider; + cmd.CommandText = @" - IF EXISTS (SELECT 1 FROM Customers WHERE CustomerId = @customerId) - UPDATE Customers - SET Name = @name, RegionId = @regionId - WHERE CustomerId = @customerId - ELSE - INSERT INTO Customers (CustomerId, Name, RegionId) - VALUES (@customerId, @name, @regionId)"; + IF EXISTS (SELECT 1 FROM Customers WHERE CustomerId = @customerId) + UPDATE Customers + SET Name = @name, RegionId = @regionId + WHERE CustomerId = @customerId + ELSE + INSERT INTO Customers (CustomerId, Name, RegionId) + VALUES (@customerId, @name, @regionId)"; cmd.Parameters.AddWithValue("@customerId", customerId); cmd.Parameters.AddWithValue("@name", name); cmd.Parameters.AddWithValue("@regionId", regionId); @@ -87,7 +89,7 @@ INSERT INTO Customers (CustomerId, Name, RegionId) // Execute the command cmd.ExecuteNonQuery(); } - }); + } } /// @@ -99,13 +101,13 @@ private static void AddOrder( int customerId, int productId) { - SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() => + // Looks up the key in the shard map and opens a connection to the shard + using (SqlConnection conn = shardMap.OpenConnectionForKey(customerId, credentialsConnectionString)) { - // Looks up the key in the shard map and opens a connection to the shard - using (SqlConnection conn = shardMap.OpenConnectionForKey(customerId, credentialsConnectionString)) + // Create a simple command that will insert a new order + using (SqlCommand cmd = conn.CreateCommand()) { - // Create a simple command that will insert a new order - SqlCommand cmd = conn.CreateCommand(); + cmd.RetryLogicProvider = SqlDatabaseUtils.SqlRetryProvider; // Create a simple command cmd.CommandText = @"INSERT INTO dbo.Orders (CustomerId, OrderDate, ProductId) @@ -118,7 +120,7 @@ private static void AddOrder( // Execute the command cmd.ExecuteNonQuery(); } - }); + } ConsoleUtils.WriteInfo("Inserted order for customer ID: {0}", customerId); } @@ -137,4 +139,4 @@ private static int GetCustomerId(int maxid) return s_r.Next(0, maxid); } } -} +} \ No newline at end of file diff --git a/Samples/ElasticScaleStarterKit/ElasticScaleStarterKit.csproj b/Samples/ElasticScaleStarterKit/ElasticScaleStarterKit.csproj index d9bfc74..e37f36e 100644 --- a/Samples/ElasticScaleStarterKit/ElasticScaleStarterKit.csproj +++ b/Samples/ElasticScaleStarterKit/ElasticScaleStarterKit.csproj @@ -5,9 +5,6 @@ - - - @@ -18,6 +15,11 @@ - + + + + + Always + \ No newline at end of file diff --git a/Samples/ElasticScaleStarterKit/LICENSE b/Samples/ElasticScaleStarterKit/LICENSE index b8b569d..06f7e30 100644 --- a/Samples/ElasticScaleStarterKit/LICENSE +++ b/Samples/ElasticScaleStarterKit/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Microsoft +Copyright (c) 2024 Microsoft Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Samples/ElasticScaleStarterKit/Program.cs b/Samples/ElasticScaleStarterKit/Program.cs index 2a87424..f25c02c 100644 --- a/Samples/ElasticScaleStarterKit/Program.cs +++ b/Samples/ElasticScaleStarterKit/Program.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.Diagnostics; using System.Linq; using Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement; @@ -19,6 +20,8 @@ public static void Main() Console.WriteLine("*** Welcome to Elastic Database Tools Starter Kit ***"); Console.WriteLine("***********************************************************"); Console.WriteLine(); + Console.WriteLine("Authentication method used: {0}", ConfigurationManager.AppSettings["SqlAuthenticationMethod"]); + Console.WriteLine(); // Verify that we can connect to the Sql Database that is specified in App.config settings if (!SqlDatabaseUtils.TryConnectToSqlDatabase()) diff --git a/Samples/ElasticScaleStarterKit/README.txt b/Samples/ElasticScaleStarterKit/README.txt index d11ba40..d3f42b3 100644 --- a/Samples/ElasticScaleStarterKit/README.txt +++ b/Samples/ElasticScaleStarterKit/README.txt @@ -1,12 +1,17 @@ - -Prerequisites: -- Visual Studio 2012 or later, Professional Edition or higher -- Nuget 2.7 or later -- .NET Framework 4.5 or later -- Microsoft Azure SQL Database +Prerequisites +------------- +- .Net 6.0 or later +- Visual Studio 2022 +- Microsoft Azure SQL Database or Microsoft SQL Server instance on local machine +Getting Started +--------------- Before running this project, please fill in the values in App.config. For detailed instructions and background information, please refer to Getting Started web page for Azure SQL Database Elastic Scale: http://go.microsoft.com/fwlink/?LinkID=510913 +Authentication Info +------------------- +https://learn.microsoft.com/en-us/sql/connect/ado-net/sql/azure-active-directory-authentication?view=sql-server-ver16#setting-microsoft-entra-authentication +https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-overview?view=azuresql#connect-by-using-microsoft-entra-identities \ No newline at end of file diff --git a/Samples/ElasticScaleStarterKit/SqlDatabaseUtils.cs b/Samples/ElasticScaleStarterKit/SqlDatabaseUtils.cs index 6049668..e5566e0 100644 --- a/Samples/ElasticScaleStarterKit/SqlDatabaseUtils.cs +++ b/Samples/ElasticScaleStarterKit/SqlDatabaseUtils.cs @@ -3,12 +3,10 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Data.SqlClient; using System.IO; using System.Text; using System.Threading; -using Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling; +using Microsoft.Data.SqlClient; namespace ElasticScaleStarterKit { @@ -22,6 +20,22 @@ internal static class SqlDatabaseUtils /// public const string MasterDatabaseName = "master"; + // Create a retry logic provider + public static SqlRetryLogicBaseProvider SqlRetryProvider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(SqlRetryPolicy); + + /// + /// Gets the retry policy to use for connections to SQL Server. + /// + private static SqlRetryLogicOption SqlRetryPolicy => new() + { + // Tries 5 times before throwing an exception + NumberOfTries = 5, + // Preferred gap time to delay before retry + DeltaTime = TimeSpan.FromSeconds(1), + // Maximum gap time for each delay time before retry + MaxTimeInterval = TimeSpan.FromSeconds(20) + }; + /// /// Returns true if we can connect to the database. /// @@ -34,11 +48,9 @@ public static bool TryConnectToSqlDatabase() try { - using (ReliableSqlConnection conn = new ReliableSqlConnection( - connectionString, - SqlRetryPolicy, - SqlRetryPolicy)) + using (SqlConnection conn = new SqlConnection(connectionString)) { + conn.RetryLogicProvider = SqlRetryProvider; conn.Open(); } @@ -55,91 +67,96 @@ public static bool TryConnectToSqlDatabase() public static bool DatabaseExists(string server, string db) { - using (ReliableSqlConnection conn = new ReliableSqlConnection( - Configuration.GetConnectionString(server, MasterDatabaseName), - SqlRetryPolicy, - SqlRetryPolicy)) + using (SqlConnection conn = new SqlConnection(Configuration.GetConnectionString(server, MasterDatabaseName))) { + conn.RetryLogicProvider = SqlRetryProvider; conn.Open(); - SqlCommand cmd = conn.CreateCommand(); - cmd.CommandText = "select count(*) from sys.databases where name = @dbname"; - cmd.Parameters.AddWithValue("@dbname", db); - cmd.CommandTimeout = 60; - int count = conn.ExecuteCommand(cmd); + using (SqlCommand cmd = conn.CreateCommand()) + { + cmd.RetryLogicProvider = SqlRetryProvider; + + cmd.CommandText = "select count(*) from sys.databases where name = @dbname"; + cmd.Parameters.AddWithValue("@dbname", db); + cmd.CommandTimeout = 60; + + int count = (int)cmd.ExecuteScalar(); - bool exists = count > 0; - return exists; + bool exists = count > 0; + return exists; + } } } public static bool DatabaseIsOnline(string server, string db) { - using (ReliableSqlConnection conn = new ReliableSqlConnection( - Configuration.GetConnectionString(server, MasterDatabaseName), - SqlRetryPolicy, - SqlRetryPolicy)) + using (SqlConnection conn = new SqlConnection(Configuration.GetConnectionString(server, MasterDatabaseName))) { + conn.RetryLogicProvider = SqlRetryProvider; conn.Open(); - SqlCommand cmd = conn.CreateCommand(); - cmd.CommandText = "select count(*) from sys.databases where name = @dbname and state = 0"; // online - cmd.Parameters.AddWithValue("@dbname", db); - cmd.CommandTimeout = 60; - int count = conn.ExecuteCommand(cmd); + using (SqlCommand cmd = conn.CreateCommand()) + { + cmd.RetryLogicProvider = SqlRetryProvider; + + cmd.CommandText = "select count(*) from sys.databases where name = @dbname and state = 0"; // online + cmd.Parameters.AddWithValue("@dbname", db); + cmd.CommandTimeout = 60; + + int count = (int)cmd.ExecuteScalar(); - bool exists = count > 0; - return exists; + bool exists = count > 0; + return exists; + } } } public static void CreateDatabase(string server, string db) { ConsoleUtils.WriteInfo("Creating database {0}", db); - using (ReliableSqlConnection conn = new ReliableSqlConnection( - Configuration.GetConnectionString(server, MasterDatabaseName), - SqlRetryPolicy, - SqlRetryPolicy)) + using (SqlConnection conn = new SqlConnection(Configuration.GetConnectionString(server, MasterDatabaseName))) { + conn.RetryLogicProvider = SqlRetryProvider; conn.Open(); - SqlCommand cmd = conn.CreateCommand(); - - // Determine if we are connecting to Azure SQL DB - cmd.CommandText = "SELECT SERVERPROPERTY('EngineEdition')"; - cmd.CommandTimeout = 60; - int engineEdition = conn.ExecuteCommand(cmd); - if (engineEdition == 5) + using (SqlCommand cmd = conn.CreateCommand()) { - // Azure SQL DB - SqlRetryPolicy.ExecuteAction(() => + // Determine if we are connecting to Azure SQL DB + cmd.CommandText = "SELECT SERVERPROPERTY('EngineEdition')"; + cmd.CommandTimeout = 60; + cmd.RetryLogicProvider = SqlRetryProvider; + + int engineEdition = (int)cmd.ExecuteScalar(); + + if (engineEdition == 5) + { + // Azure SQL DB + if (!DatabaseExists(server, db)) + { + // Begin creation (which is async for Standard/Premium editions) + cmd.CommandText = string.Format( + "CREATE DATABASE {0} (EDITION = '{1}')", + BracketEscapeName(db), + Configuration.DatabaseEdition); + cmd.CommandTimeout = 180; + cmd.ExecuteNonQuery(); + } + + // Wait for the operation to complete + while (!DatabaseIsOnline(server, db)) { - if (!DatabaseExists(server, db)) - { - // Begin creation (which is async for Standard/Premium editions) - cmd.CommandText = string.Format( - "CREATE DATABASE {0} (EDITION = '{1}')", - BracketEscapeName(db), - Configuration.DatabaseEdition); - cmd.CommandTimeout = 60; - cmd.ExecuteNonQuery(); - } - }); - - // Wait for the operation to complete - while (!DatabaseIsOnline(server, db)) + ConsoleUtils.WriteInfo("Waiting for database {0} to come online...", db); + Thread.Sleep(TimeSpan.FromSeconds(5)); + } + + ConsoleUtils.WriteInfo("Database {0} is online", db); + } + else { - ConsoleUtils.WriteInfo("Waiting for database {0} to come online...", db); - Thread.Sleep(TimeSpan.FromSeconds(5)); + // Other edition of SQL DB + cmd.CommandText = string.Format("CREATE DATABASE {0}", BracketEscapeName(db)); + cmd.ExecuteNonQuery(); } - - ConsoleUtils.WriteInfo("Database {0} is online", db); - } - else - { - // Other edition of SQL DB - cmd.CommandText = string.Format("CREATE DATABASE {0}", BracketEscapeName(db)); - conn.ExecuteCommand(cmd); } } } @@ -147,34 +164,33 @@ public static void CreateDatabase(string server, string db) public static void DropDatabase(string server, string db) { ConsoleUtils.WriteInfo("Dropping database {0}", db); - using (ReliableSqlConnection conn = new ReliableSqlConnection( - Configuration.GetConnectionString(server, MasterDatabaseName), - SqlRetryPolicy, - SqlRetryPolicy)) + using (SqlConnection conn = new SqlConnection(Configuration.GetConnectionString(server, MasterDatabaseName))) { + conn.RetryLogicProvider = SqlRetryProvider; conn.Open(); - SqlCommand cmd = conn.CreateCommand(); - // Determine if we are connecting to Azure SQL DB - cmd.CommandText = "SELECT SERVERPROPERTY('EngineEdition')"; - cmd.CommandTimeout = 60; - int engineEdition = conn.ExecuteCommand(cmd); - - // Drop the database - if (engineEdition == 5) + using (SqlCommand cmd = conn.CreateCommand()) { - // Azure SQL DB + // Determine if we are connecting to Azure SQL DB + cmd.CommandText = "SELECT SERVERPROPERTY('EngineEdition')"; + cmd.CommandTimeout = 60; + int engineEdition = (int)cmd.ExecuteScalar(); - cmd.CommandText = string.Format("DROP DATABASE {0}", BracketEscapeName(db)); - cmd.ExecuteNonQuery(); - } - else - { - cmd.CommandText = string.Format( - @"ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE - DROP DATABASE {0}", - BracketEscapeName(db)); - cmd.ExecuteNonQuery(); + // Drop the database + if (engineEdition == 5) + { + // Azure SQL DB + cmd.CommandText = string.Format("DROP DATABASE {0}", BracketEscapeName(db)); + cmd.ExecuteNonQuery(); + } + else + { + cmd.CommandText = string.Format( + @"ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE + DROP DATABASE {0}", + BracketEscapeName(db)); + cmd.ExecuteNonQuery(); + } } } } @@ -182,22 +198,24 @@ public static void DropDatabase(string server, string db) public static void ExecuteSqlScript(string server, string db, string schemaFile) { ConsoleUtils.WriteInfo("Executing script {0}", schemaFile); - using (ReliableSqlConnection conn = new ReliableSqlConnection( - Configuration.GetConnectionString(server, db), - SqlRetryPolicy, - SqlRetryPolicy)) + using (SqlConnection conn = new SqlConnection(Configuration.GetConnectionString(server, db))) { + conn.RetryLogicProvider = SqlRetryProvider; conn.Open(); - SqlCommand cmd = conn.CreateCommand(); - // Read the commands from the sql script file - IEnumerable commands = ReadSqlScript(schemaFile); - - foreach (string command in commands) + using (SqlCommand cmd = conn.CreateCommand()) { - cmd.CommandText = command; - cmd.CommandTimeout = 60; - conn.ExecuteCommand(cmd); + cmd.RetryLogicProvider = SqlRetryProvider; + + // Read the commands from the sql script file + IEnumerable commands = ReadSqlScript(schemaFile); + + foreach (string command in commands) + { + cmd.CommandText = command; + cmd.CommandTimeout = 60; + cmd.ExecuteNonQuery(); + } } } } @@ -233,69 +251,5 @@ private static string BracketEscapeName(string sqlName) { return '[' + sqlName.Replace("]", "]]") + ']'; } - - /// - /// Gets the retry policy to use for connections to SQL Server. - /// - public static RetryPolicy SqlRetryPolicy - { - get - { - return new RetryPolicy(10, TimeSpan.FromSeconds(5)); - } - } - - /// - /// Extended sql transient error detection strategy that performs additional transient error - /// checks besides the ones done by the enterprise library. - /// - private class ExtendedSqlDatabaseTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy - { - /// - /// Enterprise transient error detection strategy. - /// - private SqlDatabaseTransientErrorDetectionStrategy _sqltransientErrorDetectionStrategy = new SqlDatabaseTransientErrorDetectionStrategy(); - - /// - /// Checks with enterprise library's default handler to see if the error is transient, additionally checks - /// for such errors using the code in the in function. - /// - /// Exception being checked. - /// true if exception is considered transient, false otherwise. - public bool IsTransient(Exception ex) - { - return _sqltransientErrorDetectionStrategy.IsTransient(ex) || IsTransientException(ex); - } - - /// - /// Detects transient errors not currently considered as transient by the enterprise library's strategy. - /// - /// Input exception. - /// true if exception is considered transient, false otherwise. - private static bool IsTransientException(Exception ex) - { - SqlException se = ex as SqlException; - - if (se != null && se.InnerException != null) - { - Win32Exception we = se.InnerException as Win32Exception; - - if (we != null) - { - switch (we.NativeErrorCode) - { - case 0x102: - // Transient wait expired error resulting in timeout - return true; - case 0x121: - // Transient semaphore wait expired error resulting in timeout - return true; - } - } - } - - return false; - } - } } -} +} \ No newline at end of file diff --git a/Samples/ElasticScaleStarterKit/appsettings.json b/Samples/ElasticScaleStarterKit/appsettings.json deleted file mode 100644 index 3bb3c89..0000000 --- a/Samples/ElasticScaleStarterKit/appsettings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ServerName": "(localdb)\\v11.0", - "IntegratedSecurity": true, - "UserName": "MyUserName", - "Password": "MyPassword", - "DatabaseEdition": "Basic" -} \ No newline at end of file diff --git a/Samples/ShardSqlCmd/LICENSE b/Samples/ShardSqlCmd/LICENSE index b8b569d..06f7e30 100644 --- a/Samples/ShardSqlCmd/LICENSE +++ b/Samples/ShardSqlCmd/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Microsoft +Copyright (c) 2024 Microsoft Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Samples/ShardSqlCmd/Program.cs b/Samples/ShardSqlCmd/Program.cs index 5ce8a42..7dde625 100644 --- a/Samples/ShardSqlCmd/Program.cs +++ b/Samples/ShardSqlCmd/Program.cs @@ -5,12 +5,12 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; -using System.Data.SqlClient; using System.Diagnostics; using System.Linq; using System.Text; using Microsoft.Azure.SqlDatabase.ElasticScale.Query; using Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement; +using Microsoft.Data.SqlClient; namespace ShardSqlCmd { diff --git a/Samples/ShardSqlCmd/ShardSqlCmd.csproj b/Samples/ShardSqlCmd/ShardSqlCmd.csproj index 5457ab3..7e4d5a9 100644 --- a/Samples/ShardSqlCmd/ShardSqlCmd.csproj +++ b/Samples/ShardSqlCmd/ShardSqlCmd.csproj @@ -1,14 +1,13 @@  - net6.0;net8.0 Exe - + - + diff --git a/Src/ElasticScale.Client/Microsoft.Azure.SqlDatabase.ElasticScale.Client.csproj b/Src/ElasticScale.Client/Microsoft.Azure.SqlDatabase.ElasticScale.Client.csproj index 4c70550..e10e48e 100644 --- a/Src/ElasticScale.Client/Microsoft.Azure.SqlDatabase.ElasticScale.Client.csproj +++ b/Src/ElasticScale.Client/Microsoft.Azure.SqlDatabase.ElasticScale.Client.csproj @@ -5,11 +5,11 @@ © Microsoft Corporation. All rights reserved. Microsoft Azure SQL Database: Elastic Database Client Library en-US - 2.4.1 + 2.4.2-preview1 Microsoft - net6.0;net8.0 + netstandard2.0;net6.0 Microsoft;Elastic;Scale;Azure;SQL;DB;Database;Shard;Sharding;Management;Query;azureofficial - Updated dependent nugets (due to CVEs), added support for ActiveDirectoryDefault Entra auth + Elastic Scale Client now targets .Net Standard 2.0 along with .Net 6.0 and sample apps are updated to support Entra auth Icon.png https://github.com/Azure/elastic-db-tools MIT diff --git a/Test/ElasticScale.ClientTestCommon/Microsoft.Azure.SqlDatabase.ElasticScale.ClientTestCommon.csproj b/Test/ElasticScale.ClientTestCommon/Microsoft.Azure.SqlDatabase.ElasticScale.ClientTestCommon.csproj index b108ad6..a069890 100644 --- a/Test/ElasticScale.ClientTestCommon/Microsoft.Azure.SqlDatabase.ElasticScale.ClientTestCommon.csproj +++ b/Test/ElasticScale.ClientTestCommon/Microsoft.Azure.SqlDatabase.ElasticScale.ClientTestCommon.csproj @@ -1,6 +1,6 @@  - net6.0;net8.0 + netstandard2.0 $(NoWarn);SYSLIB0011; diff --git a/Test/ElasticScale.Query.UnitTests/Microsoft.Azure.SqlDatabase.ElasticScale.Query.UnitTests.csproj b/Test/ElasticScale.Query.UnitTests/Microsoft.Azure.SqlDatabase.ElasticScale.Query.UnitTests.csproj index acad1fa..5e2aecb 100644 --- a/Test/ElasticScale.Query.UnitTests/Microsoft.Azure.SqlDatabase.ElasticScale.Query.UnitTests.csproj +++ b/Test/ElasticScale.Query.UnitTests/Microsoft.Azure.SqlDatabase.ElasticScale.Query.UnitTests.csproj @@ -1,6 +1,6 @@  - net6.0;net8.0 + net6.0;net8.0;net481 true false $(NoWarn);CS8073; @@ -8,7 +8,6 @@ - diff --git a/Test/ElasticScale.Query.UnitTests/MultiShardQueryE2ETests.cs b/Test/ElasticScale.Query.UnitTests/MultiShardQueryE2ETests.cs index 72e6924..a53f702 100644 --- a/Test/ElasticScale.Query.UnitTests/MultiShardQueryE2ETests.cs +++ b/Test/ElasticScale.Query.UnitTests/MultiShardQueryE2ETests.cs @@ -24,533 +24,579 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.Azure.SqlDatabase.ElasticScale.Query.UnitTests; - -/// -/// Tests for end to end scenarios where a user -/// connects to his shards, executes commands against them -/// and receives results -/// -[TestClass] -public class MultiShardQueryE2ETests +namespace Microsoft.Azure.SqlDatabase.ElasticScale.Query.UnitTests { - #region Global vars - - - /// - /// Handle on connections to all shards - /// - private MultiShardConnection _shardConnection; - /// - /// Handle to the ShardMap with our Test databases. + /// Tests for end to end scenarios where a user + /// connects to his shards, executes commands against them + /// and receives results /// - private ShardMap _shardMap; - - #endregion - - #region Boilerplate - - /// - /// Gets or sets the test context which provides - /// information about and functionality for the current test run. - /// - public TestContext TestContext { get; set; } - - [ClassInitialize()] - public static void MyClassInitialize(TestContext testContext) + [TestClass] + public class MultiShardQueryE2ETests { - // Drop and recreate the test databases, tables, and data that we will use to verify - // the functionality. - // For now I have hardcoded the server location and database names. A better approach would be - // to make the server location configurable and the database names be guids. - // Not the top priority right now, though. - // - SqlConnection.ClearAllPools(); - MultiShardTestUtils.DropAndCreateDatabases(); - MultiShardTestUtils.CreateAndPopulateTables(); - } + #region Global vars - /// - /// Blow away our three test databases that we drove the tests off of. - /// Doing this so that we don't leave objects littered around. - /// - [ClassCleanup()] - public static void MyClassCleanup() - { - // We need to clear the connection pools so that we don't get a database still in use error - // resulting from our attenpt to drop the databases below. - // - SqlConnection.ClearAllPools(); - MultiShardTestUtils.DropDatabases(); - } - /// - /// Open up a clean connection to each test database prior to each test. - /// - [TestInitialize()] - public void MyTestInitialize() - { - _shardMap = MultiShardTestUtils.CreateAndGetTestShardMap(); + /// + /// Handle on connections to all shards + /// + private MultiShardConnection _shardConnection; - _shardConnection = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString); - } + /// + /// Handle to the ShardMap with our Test databases. + /// + private ShardMap _shardMap; - /// - /// Close our connections to each test database after each test. - /// - [TestCleanup()] - public void MyTestCleanup() => - // Close connections after each test. - // - _shardConnection.Dispose(); + #endregion - #endregion + #region Boilerplate - /// - /// Check that we can iterate through 3 result sets as expected. - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestSimpleSelect_PartialResults() => TestSimpleSelect(MultiShardExecutionPolicy.PartialResults); + /// + /// Gets or sets the test context which provides + /// information about and functionality for the current test run. + /// + public TestContext TestContext { get; set; } - /// - /// Check that we can iterate through 3 result sets as expected. - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestSimpleSelect_CompleteResults() => TestSimpleSelect(MultiShardExecutionPolicy.CompleteResults); + [ClassInitialize()] + public static void MyClassInitialize(TestContext testContext) + { + // Drop and recreate the test databases, tables, and data that we will use to verify + // the functionality. + // For now I have hardcoded the server location and database names. A better approach would be + // to make the server location configurable and the database names be guids. + // Not the top priority right now, though. + // + SqlConnection.ClearAllPools(); + MultiShardTestUtils.DropAndCreateDatabases(); + MultiShardTestUtils.CreateAndPopulateTables(); + } - public void TestSimpleSelect(MultiShardExecutionPolicy policy) - { - // What we're doing: - // Grab all rows from each test database. - // Load them into a MultiShardDataReader. - // Iterate through the rows and make sure that we have 9 total. - // - using var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString); - using var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT dbNameField, Test_int_Field, Test_bigint_Field FROM ConsistentShardedTable"; - cmd.ExecutionOptions = MultiShardExecutionOptions.IncludeShardNameColumn; - cmd.ExecutionPolicy = policy; - - using var sdr = cmd.ExecuteReader(); - var recordsRetrieved = 0; - Logger.Log("Starting to get records"); - while (sdr.Read()) + /// + /// Blow away our three test databases that we drove the tests off of. + /// Doing this so that we don't leave objects littered around. + /// + [ClassCleanup()] + public static void MyClassCleanup() { - recordsRetrieved++; - var dbNameField = sdr.GetString(0); - var testIntField = sdr.GetFieldValue(1); - var testBigIntField = sdr.GetFieldValue(2); - var shardIdPseudoColumn = sdr.GetFieldValue(3); - var logRecord = - string.Format( - "RecordRetrieved: dbNameField: {0}, TestIntField: {1}, TestBigIntField: {2}, shardIdPseudoColumnField: {3}, RecordCount: {4}", - dbNameField, testIntField, testBigIntField, shardIdPseudoColumn, recordsRetrieved); - Logger.Log(logRecord); - Debug.WriteLine(logRecord); + // We need to clear the connection pools so that we don't get a database still in use error + // resulting from our attenpt to drop the databases below. + // + SqlConnection.ClearAllPools(); + MultiShardTestUtils.DropDatabases(); } - sdr.Close(); + /// + /// Open up a clean connection to each test database prior to each test. + /// + [TestInitialize()] + public void MyTestInitialize() + { + _shardMap = MultiShardTestUtils.CreateAndGetTestShardMap(); - Assert.AreEqual(recordsRetrieved, 9); - } + _shardConnection = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString); + } + /// + /// Close our connections to each test database after each test. + /// + [TestCleanup()] + public void MyTestCleanup() => + // Close connections after each test. + // + _shardConnection.Dispose(); - /// - /// Check that we can return an empty result set that has a schema table - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestSelect_NoRows_CompleteResults() => TestSelectNoRows("select 1 where 0 = 1", MultiShardExecutionPolicy.CompleteResults); + #endregion - /// - /// Check that we can return an empty result set that has a schema table - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestSelect_NoRows_PartialResults() => TestSelectNoRows("select 1 where 0 = 1", MultiShardExecutionPolicy.PartialResults); + /// + /// Check that we can iterate through 3 result sets as expected. + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestSimpleSelect_PartialResults() => TestSimpleSelect(MultiShardExecutionPolicy.PartialResults); - public void TestSelectNoRows(string commandText, MultiShardExecutionPolicy policy) - { - using var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString); - using var cmd = conn.CreateCommand(); - cmd.CommandText = commandText; - cmd.ExecutionPolicy = policy; + /// + /// Check that we can iterate through 3 result sets as expected. + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestSimpleSelect_CompleteResults() => TestSimpleSelect(MultiShardExecutionPolicy.CompleteResults); - // Read first - using (var sdr = cmd.ExecuteReader()) + public void TestSimpleSelect(MultiShardExecutionPolicy policy) { - Assert.AreEqual(0, sdr.MultiShardExceptions.Count); - while (sdr.Read()) + // What we're doing: + // Grab all rows from each test database. + // Load them into a MultiShardDataReader. + // Iterate through the rows and make sure that we have 9 total. + // + using (var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString)) { - Assert.Fail("Should not have gotten any records."); - } + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "SELECT dbNameField, Test_int_Field, Test_bigint_Field FROM ConsistentShardedTable"; + cmd.ExecutionOptions = MultiShardExecutionOptions.IncludeShardNameColumn; + cmd.ExecutionPolicy = policy; - Assert.IsFalse(sdr.HasRows); - } + using (var sdr = cmd.ExecuteReader()) + { + var recordsRetrieved = 0; + Logger.Log("Starting to get records"); + while (sdr.Read()) + { + recordsRetrieved++; + var dbNameField = sdr.GetString(0); + var testIntField = sdr.GetFieldValue(1); + var testBigIntField = sdr.GetFieldValue(2); + var shardIdPseudoColumn = sdr.GetFieldValue(3); + var logRecord = + string.Format( + "RecordRetrieved: dbNameField: {0}, TestIntField: {1}, TestBigIntField: {2}, shardIdPseudoColumnField: {3}, RecordCount: {4}", + dbNameField, testIntField, testBigIntField, shardIdPseudoColumn, recordsRetrieved); + Logger.Log(logRecord); + Debug.WriteLine(logRecord); + } - // HasRows first - using (var sdr = cmd.ExecuteReader()) - { - Assert.AreEqual(0, sdr.MultiShardExceptions.Count); - Assert.IsFalse(sdr.HasRows); - while (sdr.Read()) - { - Assert.Fail("Should not have gotten any records."); + sdr.Close(); + + Assert.AreEqual(recordsRetrieved, 9); + } + } } } - } - /// - /// Check that we can return an empty result set that does not have a schema table - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestSelect_NonQuery_CompleteResults() => TestSelectNonQuery("if (0 = 1) select 1 ", MultiShardExecutionPolicy.CompleteResults); + /// + /// Check that we can return an empty result set that has a schema table + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestSelect_NoRows_CompleteResults() => TestSelectNoRows("select 1 where 0 = 1", MultiShardExecutionPolicy.CompleteResults); - /// - /// Check that we can return a completely empty result set as expected. - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestSelect_NonQuery_PartialResults() => TestSelectNonQuery("if (0 = 1) select 1", MultiShardExecutionPolicy.PartialResults); + /// + /// Check that we can return an empty result set that has a schema table + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestSelect_NoRows_PartialResults() => TestSelectNoRows("select 1 where 0 = 1", MultiShardExecutionPolicy.PartialResults); - public void TestSelectNonQuery(string commandText, MultiShardExecutionPolicy policy) - { - using var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString); - using var cmd = conn.CreateCommand(); - cmd.CommandText = commandText; - cmd.ExecutionPolicy = policy; - - using var sdr = cmd.ExecuteReader(); - Assert.AreEqual(0, sdr.MultiShardExceptions.Count); - - // TODO: This is a weird error message, but it's good enough for now - // Fixing this will require significant refactoring of MultiShardDataReader, - // we should fix it when we finish implementing async adding of child readers - _ = AssertExtensions.AssertThrows(() => sdr.Read()); - } + public void TestSelectNoRows(string commandText, MultiShardExecutionPolicy policy) + { + using (var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString)) + { + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = commandText; + cmd.ExecutionPolicy = policy; - /// - /// Check that ExecuteReader throws when all shards have an exception - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestSelect_Failure_PartialResults() - { - var e = TestSelectFailure( - "raiserror('blah', 16, 0)", - MultiShardExecutionPolicy.PartialResults); + // Read first + using (var sdr = cmd.ExecuteReader()) + { + Assert.AreEqual(0, sdr.MultiShardExceptions.Count); + while (sdr.Read()) + { + Assert.Fail("Should not have gotten any records."); + } - // All children should have failed - Assert.AreEqual(_shardMap.GetShards().Count(), e.InnerExceptions.Count); - } + Assert.IsFalse(sdr.HasRows); + } - /// - /// Check that ExecuteReader throws when all shards have an exception - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestSelect_Failure_CompleteResults() - { - var e = TestSelectFailure( - "raiserror('blah', 16, 0)", - MultiShardExecutionPolicy.CompleteResults); - - // We don't know exactly how many child exceptions will happen, because the - // first exception that is seen will cause the children to be canceled. - AssertExtensions.AssertGreaterThanOrEqualTo(1, e.InnerExceptions.Count); - AssertExtensions.AssertLessThanOrEqualTo(_shardMap.GetShards().Count(), e.InnerExceptions.Count); - } + // HasRows first + using (var sdr = cmd.ExecuteReader()) + { + Assert.AreEqual(0, sdr.MultiShardExceptions.Count); + Assert.IsFalse(sdr.HasRows); + while (sdr.Read()) + { + Assert.Fail("Should not have gotten any records."); + } + } + } + } + } + + /// + /// Check that we can return an empty result set that does not have a schema table + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestSelect_NonQuery_CompleteResults() => TestSelectNonQuery("if (0 = 1) select 1 ", MultiShardExecutionPolicy.CompleteResults); - public MultiShardAggregateException TestSelectFailure(string commandText, MultiShardExecutionPolicy policy) - { - using var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString); - using var cmd = conn.CreateCommand(); - cmd.CommandText = commandText; - cmd.ExecutionPolicy = policy; - // ExecuteReader should fail - var aggregateException = - AssertExtensions.AssertThrows(() => cmd.ExecuteReader()); + /// + /// Check that we can return a completely empty result set as expected. + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestSelect_NonQuery_PartialResults() => TestSelectNonQuery("if (0 = 1) select 1", MultiShardExecutionPolicy.PartialResults); - // Sanity check the exceptions are the correct type - foreach (var e in aggregateException.InnerExceptions) + public void TestSelectNonQuery(string commandText, MultiShardExecutionPolicy policy) { - Assert.IsInstanceOfType(e, typeof(MultiShardException)); - Assert.IsInstanceOfType(e.InnerException, typeof(SqlException)); - } + using (var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString)) + { + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = commandText; + cmd.ExecutionPolicy = policy; - // Return the exception so that the caller can do additional validation - return aggregateException; - } + using (var sdr = cmd.ExecuteReader()) + { + Assert.AreEqual(0, sdr.MultiShardExceptions.Count); - /// - /// Check that we can return a partially succeeded reader when PartialResults policy is on - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestSelect_PartialFailure_PartialResults() - { - using var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString); - using var cmd = conn.CreateCommand(); - cmd.CommandText = GetPartialFailureQuery(); - cmd.ExecutionPolicy = MultiShardExecutionPolicy.PartialResults; + // TODO: This is a weird error message, but it's good enough for now + // Fixing this will require significant refactoring of MultiShardDataReader, + // we should fix it when we finish implementing async adding of child readers + _ = AssertExtensions.AssertThrows(() => sdr.Read()); + } + } + } + } + + /// + /// Check that ExecuteReader throws when all shards have an exception + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestSelect_Failure_PartialResults() + { + var e = TestSelectFailure( + "raiserror('blah', 16, 0)", + MultiShardExecutionPolicy.PartialResults); - using var sdr = cmd.ExecuteReader(); - // Exactly one should have failed - Assert.AreEqual(1, sdr.MultiShardExceptions.Count); + // All children should have failed + Assert.AreEqual(_shardMap.GetShards().Count(), e.InnerExceptions.Count); + } - // We should be able to read - while (sdr.Read()) + /// + /// Check that ExecuteReader throws when all shards have an exception + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestSelect_Failure_CompleteResults() { + var e = TestSelectFailure( + "raiserror('blah', 16, 0)", + MultiShardExecutionPolicy.CompleteResults); + + // We don't know exactly how many child exceptions will happen, because the + // first exception that is seen will cause the children to be canceled. + AssertExtensions.AssertGreaterThanOrEqualTo(1, e.InnerExceptions.Count); + AssertExtensions.AssertLessThanOrEqualTo(_shardMap.GetShards().Count(), e.InnerExceptions.Count); } - } - /// - /// Check that we fail a partially successful command when CompleteResults policy is on - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestSelect_PartialFailure_CompleteResults() - { - var query = GetPartialFailureQuery(); - var e = TestSelectFailure(query, MultiShardExecutionPolicy.CompleteResults); + public MultiShardAggregateException TestSelectFailure(string commandText, MultiShardExecutionPolicy policy) + { + using (var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString)) + { + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = commandText; + cmd.ExecutionPolicy = policy; - // Exactly one should have failed - Assert.AreEqual(1, e.InnerExceptions.Count); - } + // ExecuteReader should fail + var aggregateException = + AssertExtensions.AssertThrows(() => cmd.ExecuteReader()); - /// - /// Gets a command that fails on one shard, but succeeds on others - /// - private string GetPartialFailureQuery() - { - var shardLocations = _shardMap.GetShards().Select(s => s.Location); + // Sanity check the exceptions are the correct type + foreach (var e in aggregateException.InnerExceptions) + { + Assert.IsInstanceOfType(e, typeof(MultiShardException)); + Assert.IsInstanceOfType(e.InnerException, typeof(SqlException)); + } - // Pick an arbitrary one of those shards - var chosenShardLocation = shardLocations.First(); + // Return the exception so that the caller can do additional validation + return aggregateException; + } + } + } - // This query assumes that the chosen shard location's db name is distinct from all others - // In other words, only one shard location should have a database equal to the chosen location - Assert.AreEqual(1, shardLocations.Count(l => l.Database.Equals(chosenShardLocation.Database))); + /// + /// Check that we can return a partially succeeded reader when PartialResults policy is on + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestSelect_PartialFailure_PartialResults() + { + using (var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString)) + { + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = GetPartialFailureQuery(); + cmd.ExecutionPolicy = MultiShardExecutionPolicy.PartialResults; - // We also assume that there is more than one shard - AssertExtensions.AssertGreaterThan(1, shardLocations.Count()); + using (var sdr = cmd.ExecuteReader()) + { + // Exactly one should have failed + Assert.AreEqual(1, sdr.MultiShardExceptions.Count); - // The command will fail only on the chosen shard - return string.Format("if db_name() = '{0}' raiserror('blah', 16, 0) else select 1", - shardLocations.First().Database); - } + // We should be able to read + while (sdr.Read()) + { + } + } + } + } + } - /// - /// Basic test for async api(s) - /// Also demonstrates the async pattern of this library - /// The Sync api is implicitly tested in MultiShardDataReaderTests::TestSimpleSelect - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestQueryShardsAsync() - { - // Create new sharded connection so we can test the OpenAsync call as well. - // - using var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString); - using var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT dbNameField, Test_int_Field, Test_bigint_Field FROM ConsistentShardedTable"; - cmd.CommandType = CommandType.Text; - - using var sdr = ExecAsync(conn, cmd).Result; - var recordsRetrieved = 0; - while (sdr.Read()) + /// + /// Check that we fail a partially successful command when CompleteResults policy is on + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestSelect_PartialFailure_CompleteResults() { - recordsRetrieved++; - var dbNameField = sdr.GetString(0); - var testIntField = sdr.GetFieldValue(1); - var testBigIntField = sdr.GetFieldValue(2); - Logger.Log("RecordRetrieved: dbNameField: {0}, TestIntField: {1}, TestBigIntField: {2}, RecordCount: {3}", - dbNameField, testIntField, testBigIntField, recordsRetrieved); - } + var query = GetPartialFailureQuery(); + var e = TestSelectFailure(query, MultiShardExecutionPolicy.CompleteResults); - Assert.AreEqual(recordsRetrieved, 9); - } + // Exactly one should have failed + Assert.AreEqual(1, e.InnerExceptions.Count); + } - /// - /// Basic test for ensuring that we include/don't include the $ShardName pseudo column as desired. - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestShardNamePseudoColumnOption() - { - var pseudoColumnOptions = new bool[2]; - pseudoColumnOptions[0] = true; - pseudoColumnOptions[1] = false; - - // do the loop over the options. - // add the excpetion handling when referencing the pseudo column - // - foreach (var pseudoColumnPresent in pseudoColumnOptions) + /// + /// Gets a command that fails on one shard, but succeeds on others + /// + private string GetPartialFailureQuery() { - using var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString); - using var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT dbNameField, Test_int_Field, Test_bigint_Field FROM ConsistentShardedTable"; - cmd.CommandType = CommandType.Text; + var shardLocations = _shardMap.GetShards().Select(s => s.Location); - cmd.ExecutionPolicy = MultiShardExecutionPolicy.CompleteResults; - cmd.ExecutionOptions = pseudoColumnPresent ? MultiShardExecutionOptions.IncludeShardNameColumn : - MultiShardExecutionOptions.None; - using var sdr = cmd.ExecuteReader(CommandBehavior.Default); - Assert.AreEqual(0, sdr.MultiShardExceptions.Count); + // Pick an arbitrary one of those shards + var chosenShardLocation = shardLocations.First(); - var recordsRetrieved = 0; + // This query assumes that the chosen shard location's db name is distinct from all others + // In other words, only one shard location should have a database equal to the chosen location + Assert.AreEqual(1, shardLocations.Count(l => l.Database.Equals(chosenShardLocation.Database))); - var expectedFieldCount = pseudoColumnPresent ? 4 : 3; - var expectedVisibleFieldCount = pseudoColumnPresent ? 4 : 3; - Assert.AreEqual(expectedFieldCount, sdr.FieldCount); - Assert.AreEqual(expectedVisibleFieldCount, sdr.VisibleFieldCount); + // We also assume that there is more than one shard + AssertExtensions.AssertGreaterThan(1, shardLocations.Count()); - while (sdr.Read()) - { - recordsRetrieved++; - var dbNameField = sdr.GetString(0); - var testIntField = sdr.GetFieldValue(1); - var testBigIntField = sdr.GetFieldValue(2); + // The command will fail only on the chosen shard + return string.Format("if db_name() = '{0}' raiserror('blah', 16, 0) else select 1", + shardLocations.First().Database); + } - try + /// + /// Basic test for async api(s) + /// Also demonstrates the async pattern of this library + /// The Sync api is implicitly tested in MultiShardDataReaderTests::TestSimpleSelect + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestQueryShardsAsync() + { + // Create new sharded connection so we can test the OpenAsync call as well. + // + using (var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString)) + { + using (var cmd = conn.CreateCommand()) { - var shardIdPseudoColumn = sdr.GetFieldValue(3); - if (!pseudoColumnPresent) + cmd.CommandText = "SELECT dbNameField, Test_int_Field, Test_bigint_Field FROM ConsistentShardedTable"; + cmd.CommandType = CommandType.Text; + + using (var sdr = ExecAsync(conn, cmd).Result) { - Assert.Fail("Should not have been able to pull the pseudo column."); + var recordsRetrieved = 0; + while (sdr.Read()) + { + recordsRetrieved++; + var dbNameField = sdr.GetString(0); + var testIntField = sdr.GetFieldValue(1); + var testBigIntField = sdr.GetFieldValue(2); + Logger.Log("RecordRetrieved: dbNameField: {0}, TestIntField: {1}, TestBigIntField: {2}, RecordCount: {3}", + dbNameField, testIntField, testBigIntField, recordsRetrieved); + } + Assert.AreEqual(recordsRetrieved, 9); } } - catch (IndexOutOfRangeException) + } + } + + /// + /// Basic test for ensuring that we include/don't include the $ShardName pseudo column as desired. + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestShardNamePseudoColumnOption() + { + var pseudoColumnOptions = new bool[2]; + pseudoColumnOptions[0] = true; + pseudoColumnOptions[1] = false; + + // do the loop over the options. + // add the excpetion handling when referencing the pseudo column + // + foreach (var pseudoColumnPresent in pseudoColumnOptions) + { + using (var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString)) { - if (pseudoColumnPresent) + using (var cmd = conn.CreateCommand()) { - Assert.Fail("Should not have encountered an exception."); + cmd.CommandText = "SELECT dbNameField, Test_int_Field, Test_bigint_Field FROM ConsistentShardedTable"; + cmd.CommandType = CommandType.Text; + + cmd.ExecutionPolicy = MultiShardExecutionPolicy.CompleteResults; + cmd.ExecutionOptions = pseudoColumnPresent ? MultiShardExecutionOptions.IncludeShardNameColumn : + MultiShardExecutionOptions.None; + using (var sdr = cmd.ExecuteReader(CommandBehavior.Default)) + { + Assert.AreEqual(0, sdr.MultiShardExceptions.Count); + + var recordsRetrieved = 0; + + var expectedFieldCount = pseudoColumnPresent ? 4 : 3; + var expectedVisibleFieldCount = pseudoColumnPresent ? 4 : 3; + Assert.AreEqual(expectedFieldCount, sdr.FieldCount); + Assert.AreEqual(expectedVisibleFieldCount, sdr.VisibleFieldCount); + + while (sdr.Read()) + { + recordsRetrieved++; + var dbNameField = sdr.GetString(0); + var testIntField = sdr.GetFieldValue(1); + var testBigIntField = sdr.GetFieldValue(2); + + try + { + var shardIdPseudoColumn = sdr.GetFieldValue(3); + if (!pseudoColumnPresent) + { + Assert.Fail("Should not have been able to pull the pseudo column."); + } + } + catch (IndexOutOfRangeException) + { + if (pseudoColumnPresent) + { + Assert.Fail("Should not have encountered an exception."); + } + } + } + + Assert.AreEqual(recordsRetrieved, 9); + } } } } - - Assert.AreEqual(recordsRetrieved, 9); } - } - /// - /// Basic test for ensuring that we don't fail due to a schema mismatch on the shards. - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestSchemaMismatchErrorPropagation() - { - // First we need to alter the schema on one of the shards - we'll choose the last one. - // - var origColName = "Test_bigint_Field"; - var newColName = "ModifiedName"; - - MultiShardTestUtils.ChangeColumnNameOnShardedTable(2, origColName, newColName); - - // Then create new sharded connection so we can test the error handling logic. - // We'll wrap this all in a try-catch-finally block so that we can change the schema back - // to what the other tests will expect it to be in the finally. - // - try + /// + /// Basic test for ensuring that we don't fail due to a schema mismatch on the shards. + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestSchemaMismatchErrorPropagation() { - using var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString); - using var cmd = conn.CreateCommand(); - // Need to do a SELECT * in order to get the column name error as a schema mismatcherror. If we name it explicitly - // we will get a command execution error instead. + // First we need to alter the schema on one of the shards - we'll choose the last one. // - cmd.CommandText = "SELECT * FROM ConsistentShardedTable"; - cmd.CommandType = CommandType.Text; + var origColName = "Test_bigint_Field"; + var newColName = "ModifiedName"; - using var sdr = ExecAsync(conn, cmd).Result; - // The number of errors we have depends on which shard executed first. - // So, we know it should be 1 OR 2. + MultiShardTestUtils.ChangeColumnNameOnShardedTable(2, origColName, newColName); + + // Then create new sharded connection so we can test the error handling logic. + // We'll wrap this all in a try-catch-finally block so that we can change the schema back + // to what the other tests will expect it to be in the finally. // - Assert.IsTrue( -sdr.MultiShardExceptions.Count is 1 or 2, - string.Format("Expected 1 or 2 execution erros, but saw {0}", sdr.MultiShardExceptions.Count)); + try + { + using (var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString)) + { + using (var cmd = conn.CreateCommand()) + { + // Need to do a SELECT * in order to get the column name error as a schema mismatcherror. If we name it explicitly + // we will get a command execution error instead. + // + cmd.CommandText = "SELECT * FROM ConsistentShardedTable"; + cmd.CommandType = CommandType.Text; - var recordsRetrieved = 0; - while (sdr.Read()) + using (var sdr = ExecAsync(conn, cmd).Result) + { + // The number of errors we have depends on which shard executed first. + // So, we know it should be 1 OR 2. + // + Assert.IsTrue( + sdr.MultiShardExceptions.Count == 1 || sdr.MultiShardExceptions.Count == 2, + string.Format("Expected 1 or 2 execution erros, but saw {0}", sdr.MultiShardExceptions.Count)); + + var recordsRetrieved = 0; + while (sdr.Read()) + { + recordsRetrieved++; + var dbNameField = sdr.GetString(0); + } + + // We should see 9 records less 3 for each one that had a schema error. + var expectedRecords = 9 - (3 * sdr.MultiShardExceptions.Count); + + Assert.AreEqual(recordsRetrieved, expectedRecords); + } + } + } + } + finally { - recordsRetrieved++; - var dbNameField = sdr.GetString(0); + MultiShardTestUtils.ChangeColumnNameOnShardedTable(2, newColName, origColName); } - - // We should see 9 records less 3 for each one that had a schema error. - var expectedRecords = 9 - (3 * sdr.MultiShardExceptions.Count); - - Assert.AreEqual(recordsRetrieved, expectedRecords); - } - finally - { - MultiShardTestUtils.ChangeColumnNameOnShardedTable(2, newColName, origColName); } - } - - private async Task ExecAsync(MultiShardConnection conn, MultiShardCommand cmd) - { - cmd.ExecutionPolicy = MultiShardExecutionPolicy.PartialResults; - cmd.ExecutionOptions = MultiShardExecutionOptions.IncludeShardNameColumn; - return await cmd.ExecuteReaderAsync(CommandBehavior.Default, CancellationToken.None); - } - - /// - /// Try connecting to a non-existant shard - /// Verify exception is propagated to the user - /// - [TestMethod] - [ExpectedException(typeof(SqlException))] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestQueryShardsInvalidConnectionSync() - { - var badShard = new ShardLocation("badLocation", "badDatabase"); - var bldr = new SqlConnectionStringBuilder + private async Task ExecAsync(MultiShardConnection conn, MultiShardCommand cmd) { - DataSource = badShard.DataSource, - InitialCatalog = badShard.Database - }; - var badConn = new SqlConnection(bldr.ConnectionString); - try - { - using var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString); - conn.GetShardConnections().Add(new Tuple(badShard, - badConn)); - using var cmd = conn.CreateCommand(); - cmd.CommandText = "select 1"; - _ = cmd.ExecuteReader(); + cmd.ExecutionPolicy = MultiShardExecutionPolicy.PartialResults; + cmd.ExecutionOptions = MultiShardExecutionOptions.IncludeShardNameColumn; + + return await cmd.ExecuteReaderAsync(CommandBehavior.Default, CancellationToken.None); } - catch (Exception ex) + + /// + /// Try connecting to a non-existant shard + /// Verify exception is propagated to the user + /// + [TestMethod] + [ExpectedException(typeof(SqlException))] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestQueryShardsInvalidConnectionSync() { - if (ex is MultiShardAggregateException maex) + var badShard = new ShardLocation("badLocation", "badDatabase"); + var bldr = new SqlConnectionStringBuilder { - Logger.Log("Exception encountered: " + maex.ToString()); - throw ((MultiShardException)maex.InnerException).InnerException; + DataSource = badShard.DataSource, + InitialCatalog = badShard.Database + }; + var badConn = new SqlConnection(bldr.ConnectionString); + try + { + using (var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString)) + { + conn.GetShardConnections().Add(new Tuple(badShard, badConn)); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "select 1"; + _ = cmd.ExecuteReader(); + } + } } + catch (Exception ex) + { + if (ex is MultiShardAggregateException maex) + { + Logger.Log("Exception encountered: " + maex.ToString()); + throw ((MultiShardException)maex.InnerException).InnerException; + } - throw; + throw; + } } - } - /// - /// Tests passing a tvp as a param - /// using a datatable - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestQueryShardsTvpParam() - { - try + /// + /// Tests passing a tvp as a param + /// using a datatable + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestQueryShardsTvpParam() { - // Install schema - var createTbl = -@" + try + { + // Install schema + var createTbl = + @" CREATE TABLE dbo.PageView ( PageViewID BIGINT NOT NULL, @@ -560,8 +606,8 @@ CREATE TYPE dbo.PageViewTableType AS TABLE ( PageViewID BIGINT NOT NULL );"; - var createProc = -@"CREATE PROCEDURE dbo.procMergePageView + var createProc = + @"CREATE PROCEDURE dbo.procMergePageView @Display dbo.PageViewTableType READONLY AS BEGIN @@ -571,79 +617,81 @@ USING @Display AS S WHEN MATCHED THEN UPDATE SET T.PageViewCount = T.PageViewCount + 1 WHEN NOT MATCHED THEN INSERT VALUES(S.PageViewID, 1); END"; - using (var cmd = _shardConnection.CreateCommand()) - { - cmd.CommandText = createTbl; - cmd.ExecutionPolicy = MultiShardExecutionPolicy.PartialResults; - _ = cmd.ExecuteReader(); - - cmd.CommandText = createProc; - cmd.ExecuteNonQueryAsync(CancellationToken.None, MultiShardExecutionPolicy.PartialResults).Wait(); - } + using (var cmd = _shardConnection.CreateCommand()) + { + cmd.CommandText = createTbl; + cmd.ExecutionPolicy = MultiShardExecutionPolicy.PartialResults; + _ = cmd.ExecuteReader(); - Logger.Log("Schema installed.."); + cmd.CommandText = createProc; + cmd.ExecuteNonQueryAsync(CancellationToken.None, MultiShardExecutionPolicy.PartialResults).Wait(); + } - // Create the data table - var table = new DataTable(); - _ = table.Columns.Add("PageViewID", typeof(long)); - var idCount = 3; - for (var i = 0; i < idCount; i++) - { - _ = table.Rows.Add(i); - } + Logger.Log("Schema installed.."); - // Execute the command - using (var cmd = _shardConnection.CreateCommand()) - { - cmd.CommandType = CommandType.StoredProcedure; - cmd.CommandText = "dbo.procMergePageView"; + // Create the data table + var table = new DataTable(); + _ = table.Columns.Add("PageViewID", typeof(long)); + var idCount = 3; + for (var i = 0; i < idCount; i++) + { + _ = table.Rows.Add(i); + } - var param = new SqlParameter("@Display", table) + // Execute the command + using (var cmd = _shardConnection.CreateCommand()) { - TypeName = "dbo.PageViewTableType", - SqlDbType = SqlDbType.Structured - }; - _ = cmd.Parameters.Add(param); + cmd.CommandType = CommandType.StoredProcedure; + cmd.CommandText = "dbo.procMergePageView"; - cmd.ExecuteNonQueryAsync(CancellationToken.None, MultiShardExecutionPolicy.PartialResults).Wait(); - cmd.ExecuteNonQueryAsync(CancellationToken.None, MultiShardExecutionPolicy.PartialResults).Wait(); - } + var param = new SqlParameter("@Display", table) + { + TypeName = "dbo.PageViewTableType", + SqlDbType = SqlDbType.Structured + }; + _ = cmd.Parameters.Add(param); + + cmd.ExecuteNonQueryAsync(CancellationToken.None, MultiShardExecutionPolicy.PartialResults).Wait(); + cmd.ExecuteNonQueryAsync(CancellationToken.None, MultiShardExecutionPolicy.PartialResults).Wait(); + } - Logger.Log("Command executed.."); + Logger.Log("Command executed.."); - using (var cmd = _shardConnection.CreateCommand()) - { - // Validate that the pageviewcount was updated - cmd.CommandText = "select PageViewCount from PageView"; - cmd.CommandType = CommandType.Text; - cmd.ExecutionPolicy = MultiShardExecutionPolicy.PartialResults; - cmd.ExecutionOptions = MultiShardExecutionOptions.IncludeShardNameColumn; - using var sdr = cmd.ExecuteReader(CommandBehavior.Default); - while (sdr.Read()) + using (var cmd = _shardConnection.CreateCommand()) { - var pageCount = (long)sdr["PageViewCount"]; - Logger.Log("Page view count: {0} obtained from shard: {1}", pageCount, sdr.GetFieldValue(1)); - Assert.AreEqual(2, pageCount); + // Validate that the pageviewcount was updated + cmd.CommandText = "select PageViewCount from PageView"; + cmd.CommandType = CommandType.Text; + cmd.ExecutionPolicy = MultiShardExecutionPolicy.PartialResults; + cmd.ExecutionOptions = MultiShardExecutionOptions.IncludeShardNameColumn; + using (var sdr = cmd.ExecuteReader(CommandBehavior.Default)) + { + while (sdr.Read()) + { + var pageCount = (long)sdr["PageViewCount"]; + Logger.Log("Page view count: {0} obtained from shard: {1}", pageCount, sdr.GetFieldValue(1)); + Assert.AreEqual(2, pageCount); + } + } } } - } - catch (Exception ex) - { - if (ex is AggregateException aex) + catch (Exception ex) { - Logger.Log("Exception encountered: {0}", aex.InnerException.ToString()); + if (ex is AggregateException aex) + { + Logger.Log("Exception encountered: {0}", aex.InnerException.ToString()); + } + else + { + Logger.Log(ex.Message); + } + + throw; } - else + finally { - Logger.Log(ex.Message); - } - - throw; - } - finally - { - var dropSchema = -@"if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[procMergePageView]') and objectproperty(id, N'IsProcedure') = 1) + var dropSchema = + @"if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[procMergePageView]') and objectproperty(id, N'IsProcedure') = 1) begin drop procedure dbo.procMergePageView end @@ -655,351 +703,374 @@ drop table dbo.Pageview begin drop type dbo.PageViewTableType end"; - using var cmd = _shardConnection.CreateCommand(); - cmd.CommandText = dropSchema; - cmd.ExecuteNonQueryAsync(CancellationToken.None, MultiShardExecutionPolicy.PartialResults).Wait(); + using (var cmd = _shardConnection.CreateCommand()) + { + cmd.CommandText = dropSchema; + cmd.ExecuteNonQueryAsync(CancellationToken.None, MultiShardExecutionPolicy.PartialResults).Wait(); + } + } } - } - /// - /// Verifies that the command cancellation events are fired - /// upon cancellation of a command that is in progress - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestQueryShardsCommandCancellationHandler() - { - var cancelledShards = new List(); - var cts = new CancellationTokenSource(); - - using var cmd = _shardConnection.CreateCommand(); - var barrier = new Barrier(cmd.Connection.Shards.Count() + 1); + /// + /// Verifies that the command cancellation events are fired + /// upon cancellation of a command that is in progress + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestQueryShardsCommandCancellationHandler() + { + var cancelledShards = new List(); + var cts = new CancellationTokenSource(); - // If the threads don't meet the barrier by this time, then give up and fail the test - var barrierTimeout = TimeSpan.FromSeconds(10); + using (var cmd = _shardConnection.CreateCommand()) + { + var barrier = new Barrier(cmd.Connection.Shards.Count() + 1); - cmd.CommandText = "WAITFOR DELAY '00:01:00'"; - cmd.CommandTimeoutPerShard = 12; + // If the threads don't meet the barrier by this time, then give up and fail the test + var barrierTimeout = TimeSpan.FromSeconds(10); - cmd.ShardExecutionCanceled += (obj, args) => - { - cancelledShards.Add(args.ShardLocation); - }; + cmd.CommandText = "WAITFOR DELAY '00:01:00'"; + cmd.CommandTimeoutPerShard = 12; - cmd.ShardExecutionBegan += (obj, args) => - { - // If ShardExecutionBegan were only signaled by one thread, - // then this would hang forever. - _ = barrier.SignalAndWait(barrierTimeout); - }; + cmd.ShardExecutionCanceled += (obj, args) => + { + cancelledShards.Add(args.ShardLocation); + }; - Task cmdTask = cmd.ExecuteReaderAsync(cts.Token); + cmd.ShardExecutionBegan += (obj, args) => + { + // If ShardExecutionBegan were only signaled by one thread, + // then this would hang forever. + _ = barrier.SignalAndWait(barrierTimeout); + }; - var syncronized = barrier.SignalAndWait(barrierTimeout); - Assert.IsTrue(syncronized); + Task cmdTask = cmd.ExecuteReaderAsync(cts.Token); - // Cancel the command once execution begins - // Sleeps are bad but this is just to really make sure - // sqlclient has had a chance to begin command execution - // Will not effect the test outcome - Thread.Sleep(TimeSpan.FromSeconds(2)); - cts.Cancel(); + var syncronized = barrier.SignalAndWait(barrierTimeout); + Assert.IsTrue(syncronized); - // Validate that the task was cancelled - _ = AssertExtensions.WaitAndAssertThrows(cmdTask); + // Cancel the command once execution begins + // Sleeps are bad but this is just to really make sure + // sqlclient has had a chance to begin command execution + // Will not effect the test outcome + Thread.Sleep(TimeSpan.FromSeconds(2)); + cts.Cancel(); - // Validate that the cancellation event was fired for all shards - var allShards = _shardConnection.GetShardConnections().Select(l => l.Item1).ToList(); - CollectionAssert.AreEquivalent(allShards, cancelledShards, "Expected command canceled event to be fired for all shards!"); - } + // Validate that the task was cancelled + _ = AssertExtensions.WaitAndAssertThrows(cmdTask); - /// - /// Close the connection to one of the shards behind - /// MultiShardConnection's back. Verify that we reopen the connection with the built-in retry policy - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestQueryShardsInvalidShardStateSync() - { - // Get a shard and close it's connection - var shardSqlConnections = _shardConnection.GetShardConnections(); - shardSqlConnections[1].Item2.Close(); + // Validate that the cancellation event was fired for all shards + var allShards = _shardConnection.GetShardConnections().Select(l => l.Item1).ToList(); + CollectionAssert.AreEquivalent(allShards, cancelledShards, "Expected command canceled event to be fired for all shards!"); + } + } - try + /// + /// Close the connection to one of the shards behind + /// MultiShardConnection's back. Verify that we reopen the connection with the built-in retry policy + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestQueryShardsInvalidShardStateSync() { - // Execute - using var cmd = _shardConnection.CreateCommand(); - cmd.CommandText = "SELECT dbNameField, Test_int_Field, Test_bigint_Field FROM ConsistentShardedTable"; - cmd.CommandType = CommandType.Text; + // Get a shard and close it's connection + var shardSqlConnections = _shardConnection.GetShardConnections(); + shardSqlConnections[1].Item2.Close(); - using var sdr = cmd.ExecuteReader(); - } - catch (Exception ex) - { - if (ex is AggregateException aex) + try { - Logger.Log("Exception encountered: " + ex.Message); - throw aex.InnerExceptions.FirstOrDefault((e) => e is InvalidOperationException); + // Execute + using (var cmd = _shardConnection.CreateCommand()) + { + cmd.CommandText = "SELECT dbNameField, Test_int_Field, Test_bigint_Field FROM ConsistentShardedTable"; + cmd.CommandType = CommandType.Text; + + using (var sdr = cmd.ExecuteReader()) + { } + } } + catch (Exception ex) + { + if (ex is AggregateException aex) + { + Logger.Log("Exception encountered: " + ex.Message); + throw aex.InnerExceptions.FirstOrDefault((e) => e is InvalidOperationException); + } - throw; + throw; + } } - } - - /// - /// Validate the MultiShardConnectionString's connectionString param. - /// - Shouldn't be null - /// - No DataSource/InitialCatalog should be set - /// - ApplicationName should be enhanced with a MSQ library - /// specific suffix and should be capped at 128 chars - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestInvalidMultiShardConnectionString() - { - MultiShardConnection conn; - try + /// + /// Validate the MultiShardConnectionString's connectionString param. + /// - Shouldn't be null + /// - No DataSource/InitialCatalog should be set + /// - ApplicationName should be enhanced with a MSQ library + /// specific suffix and should be capped at 128 chars + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestInvalidMultiShardConnectionString() { - conn = new MultiShardConnection(_shardMap.GetShards(), connectionString: null); - } - catch (Exception ex) - { - Assert.IsTrue(ex is ArgumentNullException, "Expected ArgumentNullException!"); - } + MultiShardConnection conn; - try - { - conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardMapManagerConnectionString); - } - catch (Exception ex) - { - Assert.IsTrue(ex is ArgumentException, "Expected ArgumentException!"); - } + try + { + conn = new MultiShardConnection(_shardMap.GetShards(), connectionString: null); + } + catch (Exception ex) + { + Assert.IsTrue(ex is ArgumentNullException, "Expected ArgumentNullException!"); + } - // Validate that the ApplicationName is updated properly - var applicationStringBldr = new StringBuilder(); - for (var i = 0; i < ApplicationNameHelper.MaxApplicationNameLength; i++) - { - _ = applicationStringBldr.Append('x'); - } + try + { + conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardMapManagerConnectionString); + } + catch (Exception ex) + { + Assert.IsTrue(ex is ArgumentException, "Expected ArgumentException!"); + } - var applicationName = applicationStringBldr.ToString(); - var connStringBldr = new SqlConnectionStringBuilder(MultiShardTestUtils.ShardConnectionString) - { - ApplicationName = applicationName - }; - conn = new MultiShardConnection(_shardMap.GetShards(), connStringBldr.ConnectionString); - - var updatedApplicationName = new SqlConnectionStringBuilder - (conn.GetShardConnections()[0].Item2.ConnectionString).ApplicationName; - Assert.IsTrue(updatedApplicationName.Length == ApplicationNameHelper.MaxApplicationNameLength && - updatedApplicationName.EndsWith(MultiShardConnection.ApplicationNameSuffix), "ApplicationName not appended with {0}!", - MultiShardConnection.ApplicationNameSuffix); - } + // Validate that the ApplicationName is updated properly + var applicationStringBldr = new StringBuilder(); + for (var i = 0; i < ApplicationNameHelper.MaxApplicationNameLength; i++) + { + _ = applicationStringBldr.Append('x'); + } - [TestMethod] - [ExpectedException(typeof(ArgumentException))] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestCreateConnectionWithNoShards() - { - using var conn = new MultiShardConnection(new Shard[] { }, string.Empty); - Assert.Fail("Should have failed in the MultiShardConnection c-tor."); - } + var applicationName = applicationStringBldr.ToString(); + var connStringBldr = new SqlConnectionStringBuilder(MultiShardTestUtils.ShardConnectionString) + { + ApplicationName = applicationName + }; + conn = new MultiShardConnection(_shardMap.GetShards(), connStringBldr.ConnectionString); - /// - /// Regression test for VSTS Bug# 3936154 - /// - Execute a command that will result in a failure in a loop - /// - Without the fix (disabling the command behavior)s, the test will hang and timeout. - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - [Timeout(300000)] - public void TestFailedCommandWithConnectionCloseCmdBehavior() => Parallel.For(0, 100, i => - { - try - { - using var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString); - using var cmd = conn.CreateCommand(); - cmd.CommandText = "select * from table_does_not_exist"; - cmd.CommandType = CommandType.Text; - - using var sdr = cmd.ExecuteReader(); - while (sdr.Read()) - { - } - } - catch (Exception ex) - { - Console.WriteLine("Encountered exception: {0} in iteration: {1}", - ex.ToString(), i); - } - finally - { - Console.WriteLine("Completed execution of iteration: {0}", i); - } - }); + var updatedApplicationName = new SqlConnectionStringBuilder + (conn.GetShardConnections()[0].Item2.ConnectionString).ApplicationName; + Assert.IsTrue(updatedApplicationName.Length == ApplicationNameHelper.MaxApplicationNameLength && + updatedApplicationName.EndsWith(MultiShardConnection.ApplicationNameSuffix), "ApplicationName not appended with {0}!", + MultiShardConnection.ApplicationNameSuffix); + } - /// - /// This test induces failures via a ProxyServer in order to validate that: - /// a) we are handling reader failures as expected, and - /// b) we get all-or-nothing semantics on our reads from a single row - /// - [TestMethod] - [TestCategory("ExcludeFromGatedCheckin")] - public void TestShardResultFailures() - { - var proxyServer = GetProxyServer(); + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestCreateConnectionWithNoShards() + { + using (var conn = new MultiShardConnection(new Shard[] { }, string.Empty)) + { + Assert.Fail("Should have failed in the MultiShardConnection c-tor."); + } + } - try + /// + /// Regression test for VSTS Bug# 3936154 + /// - Execute a command that will result in a failure in a loop + /// - Without the fix (disabling the command behavior)s, the test will hang and timeout. + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + [Timeout(300000)] + public void TestFailedCommandWithConnectionCloseCmdBehavior() => + Parallel.For(0, 100, i => + { + try + { + using (var conn = new MultiShardConnection(_shardMap.GetShards(), MultiShardTestUtils.ShardConnectionString)) + { + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "select * from table_does_not_exist"; + cmd.CommandType = CommandType.Text; + + using (var sdr = cmd.ExecuteReader()) + { + while (sdr.Read()) + { + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine("Encountered exception: {0} in iteration: {1}", + ex.ToString(), i); + } + finally + { + Console.WriteLine("Completed execution of iteration: {0}", i); + } + }); + + /// + /// This test induces failures via a ProxyServer in order to validate that: + /// a) we are handling reader failures as expected, and + /// b) we get all-or-nothing semantics on our reads from a single row + /// + [TestMethod] + [TestCategory("ExcludeFromGatedCheckin")] + public void TestShardResultFailures() { - // Start up the proxy server. Do it in a try so we can shut it down in the finally. - // Also, we have to generate the proxyShardconnections *AFTER* we start up the server - // so that we know what port the proxy is listening on. More on the placement - // of the connection generation below. - // - proxyServer.Start(); + var proxyServer = GetProxyServer(); - // PreKillReads is the number of successful reads to perform before killing - // all the connections. We start at 0 to test the no failure case as well. - // - for (var preKillReads = 0; preKillReads <= 10; preKillReads++) + try { - // Additionally, since we are running inside a loop, we need to regenerate the proxy shard connections each time - // so that we don't re-use dead connections. If we do that we will end up hung in the read call. + // Start up the proxy server. Do it in a try so we can shut it down in the finally. + // Also, we have to generate the proxyShardconnections *AFTER* we start up the server + // so that we know what port the proxy is listening on. More on the placement + // of the connection generation below. // - var proxyShardConnections = GetProxyShardConnections(proxyServer); - using var conn = new MultiShardConnection(proxyShardConnections); - using var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT db_name() as dbName1, REPLICATE(db_name(), 1000) as longExpr, db_name() as dbName2 FROM ConsistentShardedTable"; - cmd.CommandType = CommandType.Text; - - cmd.ExecutionPolicy = MultiShardExecutionPolicy.PartialResults; - cmd.ExecutionOptions = MultiShardExecutionOptions.IncludeShardNameColumn; - - using var sdr = cmd.ExecuteReader(CommandBehavior.Default); - var tuplesRead = 0; + proxyServer.Start(); - while (sdr.Read()) + // PreKillReads is the number of successful reads to perform before killing + // all the connections. We start at 0 to test the no failure case as well. + // + for (var preKillReads = 0; preKillReads <= 10; preKillReads++) { - // Read part of the tuple first before killing the connections and - // then attempting to read the rest of the tuple. + // Additionally, since we are running inside a loop, we need to regenerate the proxy shard connections each time + // so that we don't re-use dead connections. If we do that we will end up hung in the read call. // - tuplesRead++; - - try + var proxyShardConnections = GetProxyShardConnections(proxyServer); + using (var conn = new MultiShardConnection(proxyShardConnections)) { - // The longExpr should contain the first dbName field multiple times. - // - var dbName1 = sdr.GetString(0); - var longExpr = sdr.GetString(1); - Assert.IsTrue(longExpr.Contains(dbName1)); - - if (tuplesRead == preKillReads) + using (var cmd = conn.CreateCommand()) { - proxyServer.KillAllConnections(); + cmd.CommandText = "SELECT db_name() as dbName1, REPLICATE(db_name(), 1000) as longExpr, db_name() as dbName2 FROM ConsistentShardedTable"; + cmd.CommandType = CommandType.Text; + + cmd.ExecutionPolicy = MultiShardExecutionPolicy.PartialResults; + cmd.ExecutionOptions = MultiShardExecutionOptions.IncludeShardNameColumn; + + using (var sdr = cmd.ExecuteReader(CommandBehavior.Default)) + { + var tuplesRead = 0; + + while (sdr.Read()) + { + // Read part of the tuple first before killing the connections and + // then attempting to read the rest of the tuple. + // + tuplesRead++; + + try + { + // The longExpr should contain the first dbName field multiple times. + // + var dbName1 = sdr.GetString(0); + var longExpr = sdr.GetString(1); + Assert.IsTrue(longExpr.Contains(dbName1)); + + if (tuplesRead == preKillReads) + { + proxyServer.KillAllConnections(); + } + + // The second dbName field should be the same as the first dbName field. + // + var dbName2 = sdr.GetString(2); + Assert.AreEqual(dbName1, dbName2); + + // The shardId should contain both the first and the second dbName fields. + // + var shardId = sdr.GetString(3); + Assert.IsTrue(shardId.Contains(dbName1)); + Assert.IsTrue(shardId.Contains(dbName2)); + } + catch (Exception ex) + { + // We've seen some failures here due to an attempt to access a socket after it has + // been disposed. The only place where we are attempting to access the socket + // is in the call to proxyServer.KillAllConnections. Unfortunately, it's not clear + // what is causing that problem since it only appears to repro in the lab. + // I (errobins) would rather not blindly start changing things in the code (either + // our code above, our exception handling code here, or the proxyServer code) until + // we know which socket we are trying to access when we hit this problem. + // So, the first step I will take is to pull additional exception information + // so that we can see some more information about what went wrong the next time it repros. + // + Assert.Fail("Unexpected exception, rethrowing. Here is some info: \n Message: {0} \n Source: {1} \n StackTrace: {2}", + ex.Message, ex.Source, ex.StackTrace); + throw; + } + } + + Assert.IsTrue((tuplesRead <= preKillReads) || (0 == preKillReads), + string.Format("Tuples read was {0}, Pre-kill reads was {1}", tuplesRead, preKillReads)); + } } - - // The second dbName field should be the same as the first dbName field. - // - var dbName2 = sdr.GetString(2); - Assert.AreEqual(dbName1, dbName2); - - // The shardId should contain both the first and the second dbName fields. - // - var shardId = sdr.GetString(3); - Assert.IsTrue(shardId.Contains(dbName1)); - Assert.IsTrue(shardId.Contains(dbName2)); - } - catch (Exception ex) - { - // We've seen some failures here due to an attempt to access a socket after it has - // been disposed. The only place where we are attempting to access the socket - // is in the call to proxyServer.KillAllConnections. Unfortunately, it's not clear - // what is causing that problem since it only appears to repro in the lab. - // I (errobins) would rather not blindly start changing things in the code (either - // our code above, our exception handling code here, or the proxyServer code) until - // we know which socket we are trying to access when we hit this problem. - // So, the first step I will take is to pull additional exception information - // so that we can see some more information about what went wrong the next time it repros. - // - Assert.Fail("Unexpected exception, rethrowing. Here is some info: \n Message: {0} \n Source: {1} \n StackTrace: {2}", - ex.Message, ex.Source, ex.StackTrace); - throw; } } - - Assert.IsTrue((tuplesRead <= preKillReads) || (0 == preKillReads), - string.Format("Tuples read was {0}, Pre-kill reads was {1}", tuplesRead, preKillReads)); + } + finally + { + // Be sure to shut down the proxy server. + // + var proxyLog = proxyServer.EventLog.ToString(); + Logger.Log(proxyLog); + proxyServer.Stop(); } } - finally - { - // Be sure to shut down the proxy server. - // - var proxyLog = proxyServer.EventLog.ToString(); - Logger.Log(proxyLog); - proxyServer.Stop(); - } - } - /// - /// Helper that sets up a proxy server for us and points it at our local host, 1433 SQL Server. - /// - /// - /// The newly created proxy server for our local sql server host. - /// - /// - /// Note that we are not inducing any network delay (the first arg). We coul dchange this if desired. - /// - private ProxyServer GetProxyServer() - { - var proxy = new ProxyServer(simulatedPacketDelay: 0, simulatedInDelay: true, simulatedOutDelay: true, bufferSize: 8192) + /// + /// Helper that sets up a proxy server for us and points it at our local host, 1433 SQL Server. + /// + /// + /// The newly created proxy server for our local sql server host. + /// + /// + /// Note that we are not inducing any network delay (the first arg). We coul dchange this if desired. + /// + private ProxyServer GetProxyServer() { - RemoteEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1433) - }; + var proxy = new ProxyServer(simulatedPacketDelay: 0, simulatedInDelay: true, simulatedOutDelay: true, bufferSize: 8192) + { + RemoteEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1433) + }; - return proxy; - } + return proxy; + } - /// - /// Helper that provides us with ShardConnections based on the shard map (for the database), but routed through the proxy. - /// - /// The proxy to route the connections through. - /// - /// The List of {ShardLocation, DbConnection} tuples that we can use to instantiate our multi-shard connection. - /// - /// - /// Since our shards all reside in the local instance we can just point them at a single proxy server. If we were using - /// actual physically distributed shards, then I think we would need a separate proxy for each shard. We could - /// augment these tests to use a separate proxy per shard, if we wanted, in order to be able to simulate - /// a richer variety of failures. For now, we just simulate total failures of all shards. - /// - private List> GetProxyShardConnections(ProxyServer proxy) - { - // We'll do this by looking at our pre-existing connections and working from that. - // - var baseConnString = MultiShardTestUtils.ShardConnectionString.ToString(); - var rVal = new List>(); - foreach (var shard in _shardMap.GetShards()) + /// + /// Helper that provides us with ShardConnections based on the shard map (for the database), but routed through the proxy. + /// + /// The proxy to route the connections through. + /// + /// The List of {ShardLocation, DbConnection} tuples that we can use to instantiate our multi-shard connection. + /// + /// + /// Since our shards all reside in the local instance we can just point them at a single proxy server. If we were using + /// actual physically distributed shards, then I think we would need a separate proxy for each shard. We could + /// augment these tests to use a separate proxy per shard, if we wanted, in order to be able to simulate + /// a richer variety of failures. For now, we just simulate total failures of all shards. + /// + private List> GetProxyShardConnections(ProxyServer proxy) { - // Location doesn't really matter, so just use the same one. + // We'll do this by looking at our pre-existing connections and working from that. // - var curLoc = shard.Location; - - // The connection, however, does matter, so set up a connection - var builder = new SqlConnectionStringBuilder(baseConnString) + var baseConnString = MultiShardTestUtils.ShardConnectionString.ToString(); + var rVal = new List>(); + foreach (var shard in _shardMap.GetShards()) { - DataSource = "localhost," + proxy.LocalPort, - InitialCatalog = curLoc.Database - }; + // Location doesn't really matter, so just use the same one. + // + var curLoc = shard.Location; - var curConn = new SqlConnection(builder.ToString()); + // The connection, however, does matter, so set up a connection + var builder = new SqlConnectionStringBuilder(baseConnString) + { + DataSource = "localhost," + proxy.LocalPort, + InitialCatalog = curLoc.Database + }; - var curTuple = new Tuple(curLoc, curConn); - rVal.Add(curTuple); - } + var curConn = new SqlConnection(builder.ToString()); - return rVal; + var curTuple = new Tuple(curLoc, curConn); + rVal.Add(curTuple); + } + + return rVal; + } } } diff --git a/Test/ElasticScale.Query.UnitTests/MultiShardTestUtils.cs b/Test/ElasticScale.Query.UnitTests/MultiShardTestUtils.cs index e16cb1b..902d03c 100644 --- a/Test/ElasticScale.Query.UnitTests/MultiShardTestUtils.cs +++ b/Test/ElasticScale.Query.UnitTests/MultiShardTestUtils.cs @@ -32,7 +32,7 @@ internal static class MultiShardTestUtils /// /// User password to use when connecting to shards during a fanout query. /// - private static string s_testPassword = "dogmat1C"; + private static string s_testPassword = "J8X2ndQTZ8cvu1r"; /// /// Table name for the sharded table we will issue fanout queries against. diff --git a/Test/ElasticScale.ShardManagement.UnitTests/Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement.UnitTests.csproj b/Test/ElasticScale.ShardManagement.UnitTests/Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement.UnitTests.csproj index 9530734..7c1036d 100644 --- a/Test/ElasticScale.ShardManagement.UnitTests/Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement.UnitTests.csproj +++ b/Test/ElasticScale.ShardManagement.UnitTests/Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement.UnitTests.csproj @@ -1,6 +1,6 @@  - net6.0;net8.0 + net6.0;net8.0;net481 true false 0649;$(NoWarn) @@ -8,7 +8,6 @@ -