Fix tools service crash + improve server edition naming (#506)

* Fix https://github.com/Microsoft/vscode-mssql/issues/986 and add better server edition naming
- Fixed a number of issues related to the binding queue, specifically the fact that it didn't capture exceptions that occurred on the binding thread. This caused the thread to crash the service.
  - The root cause of the error was when you get a connection error during init of the SmoMetadataProvider which threw an exception. Because of this no Binder was created, and the code would null ref later during processing
  - Added logic to handle null ref issue and other related code
  - Added a unit test for the new error handling path, and supported still returning some value in this case
- Separately, have a fix for an issue where the edition is shown as "SQL Azure" for all Azure connections. Fixing to be "Azure SQL DB" for this case and handle when the database reports as DW or Stretch instead. This maps better to users concept of what the edition should be and avoids returning what is an outdated term.

* Messed up the test - fixing this by returning the expected return object
This commit is contained in:
Kevin Cunnane
2017-10-19 10:42:26 -07:00
committed by GitHub
parent 680f9f47d0
commit 4b66203dfc
14 changed files with 222 additions and 165 deletions

View File

@@ -29,6 +29,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
public class ConnectionService
{
public const string AdminConnectionPrefix = "ADMIN:";
private const string SqlAzureEdition = "SQL Azure";
/// <summary>
/// Singleton service instance
@@ -456,7 +457,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
EngineEditionId = serverInfo.EngineEditionId,
ServerVersion = serverInfo.ServerVersion,
ServerLevel = serverInfo.ServerLevel,
ServerEdition = serverInfo.ServerEdition,
ServerEdition = MapServerEdition(serverInfo),
IsCloud = serverInfo.IsCloud,
AzureVersion = serverInfo.AzureVersion,
OsVersion = serverInfo.OsVersion,
@@ -474,6 +475,31 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
return response;
}
private string MapServerEdition(ReliableConnectionHelper.ServerInfo serverInfo)
{
string serverEdition = serverInfo.ServerEdition;
if (string.IsNullOrWhiteSpace(serverEdition))
{
return string.Empty;
}
if (SqlAzureEdition.Equals(serverEdition, StringComparison.OrdinalIgnoreCase))
{
switch(serverInfo.EngineEditionId)
{
case (int) DatabaseEngineEdition.SqlDataWarehouse:
serverEdition = SR.AzureSqlDwEdition;
break;
case (int) DatabaseEngineEdition.SqlStretchDatabase:
serverEdition = SR.AzureSqlStretchEdition;
break;
default:
serverEdition = SR.AzureSqlDbEdition;
break;
}
}
return serverEdition;
}
/// <summary>
/// Tries to create and open a connection with the given ConnectParams.
/// </summary>

View File

@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.Utility;
using System.Linq;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
@@ -64,6 +65,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
string key,
Func<IBindingContext, CancellationToken, object> bindOperation,
Func<IBindingContext, object> timeoutOperation = null,
Func<Exception, object> errorHandler = null,
int? bindingTimeout = null,
int? waitForLockTimeout = null)
{
@@ -78,6 +80,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
Key = key,
BindOperation = bindOperation,
TimeoutOperation = timeoutOperation,
ErrorHandler = errorHandler,
BindingTimeout = bindingTimeout,
WaitForLockTimeout = waitForLockTimeout
};
@@ -92,6 +95,23 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
return queueItem;
}
/// <summary>
/// Checks if a particular binding context is connected or not
/// </summary>
/// <param name="key"></param>
public bool IsBindingContextConnected(string key)
{
lock (this.bindingContextLock)
{
IBindingContext context;
if (this.BindingContextMap.TryGetValue(key, out context))
{
return context.IsConnected;
}
return false;
}
}
/// <summary>
/// Gets or creates a binding context for the provided context key
/// </summary>
@@ -270,9 +290,20 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// run the operation in a separate thread
var bindThread = new Thread(() =>
{
result = queueItem.BindOperation(
bindingContext,
cancelToken.Token);
try
{
result = queueItem.BindOperation(
bindingContext,
cancelToken.Token);
}
catch (Exception ex)
{
Logger.Write(LogLevel.Error, "Unexpected exception on the binding queue: " + ex.ToString());
if (queueItem.ErrorHandler != null)
{
result = queueItem.ErrorHandler(ex);
}
}
}, BindingQueue<T>.QueueThreadStackSize);
bindThread.Start();
@@ -282,7 +313,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
queueItem.Result = result;
}
else
{
{
cancelToken.Cancel();
// if the task didn't complete then call the timeout callback
@@ -298,7 +329,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// wait for the operation to complete before releasing the lock
bindThread.Join();
bindingContext.BindingLock.Set();
});
}).ContinueWithOnFaulted(t => Logger.Write(LogLevel.Error, "Binding queue threw exception " + t.Exception.ToString()));
}
}
catch (Exception ex)

View File

@@ -104,6 +104,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion
{
// return the default list if the connected bind fails
return CreateDefaultCompletionItems(scriptParseInfo, scriptDocumentInfo, useLowerCaseSuggestions);
},
errorHandler: ex =>
{
// return the default list if an unexpected exception occurs
return CreateDefaultCompletionItems(scriptParseInfo, scriptDocumentInfo, useLowerCaseSuggestions);
});
return queueItem;
}

View File

@@ -27,6 +27,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
string key,
Func<IBindingContext, CancellationToken, object> bindOperation,
Func<IBindingContext, object> timeoutOperation = null,
Func<Exception, object> errorHandler = null,
int? bindingTimeout = null,
int? waitForLockTimeout = null);
}

View File

@@ -794,10 +794,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
bindingContext.Binder.Bind(
parseResults,
connInfo.ConnectionDetails.DatabaseName,
BindMode.Batch);
if (bindingContext.IsConnected && bindingContext.Binder != null)
{
bindingContext.Binder.Bind(
parseResults,
connInfo.ConnectionDetails.DatabaseName,
BindMode.Batch);
}
}
catch (ConnectionException)
{
@@ -856,8 +859,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
try
{
scriptInfo.ConnectionKey = this.BindingQueue.AddConnectionContext(info, "languageService");
scriptInfo.IsConnected = true;
scriptInfo.IsConnected = this.BindingQueue.IsBindingContextConnected(scriptInfo.ConnectionKey);
}
catch (Exception ex)
{
@@ -917,40 +919,42 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
ParseResult parseResult = Parser.Parse(
"select ",
bindingContext.ParseOptions);
if (bindingContext.IsConnected && bindingContext.Binder != null)
{
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
bindingContext.Binder.Bind(
parseResults,
info.ConnectionDetails.DatabaseName,
BindMode.Batch);
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
bindingContext.Binder.Bind(
parseResults,
info.ConnectionDetails.DatabaseName,
BindMode.Batch);
// get the completion list from SQL Parser
var suggestions = Resolver.FindCompletions(
parseResult, 1, 8,
bindingContext.MetadataDisplayInfoProvider);
// get the completion list from SQL Parser
var suggestions = Resolver.FindCompletions(
parseResult, 1, 8,
bindingContext.MetadataDisplayInfoProvider);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8);
parseResult = Parser.Parse(
"exec ",
bindingContext.ParseOptions);
parseResult = Parser.Parse(
"exec ",
bindingContext.ParseOptions);
parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
bindingContext.Binder.Bind(
parseResults,
info.ConnectionDetails.DatabaseName,
BindMode.Batch);
parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
bindingContext.Binder.Bind(
parseResults,
info.ConnectionDetails.DatabaseName,
BindMode.Batch);
// get the completion list from SQL Parser
suggestions = Resolver.FindCompletions(
parseResult, 1, 6,
bindingContext.MetadataDisplayInfoProvider);
// get the completion list from SQL Parser
suggestions = Resolver.FindCompletions(
parseResult, 1, 6,
bindingContext.MetadataDisplayInfoProvider);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6);
}
return null;
});
@@ -1103,6 +1107,16 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
Message = SR.PeekDefinitionTimedoutError,
Locations = null
};
},
errorHandler: ex =>
{
// return error result
return new DefinitionResult
{
IsErrorResult = true,
Message = ex.Message,
Locations = null
};
});
// wait for the queue item

View File

@@ -36,6 +36,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// </summary>
public Func<IBindingContext, object> TimeoutOperation { get; set; }
/// <summary>
/// Gets or sets the operation to call if the bind operation encounters an unexpected exception.
/// Supports returning an object in case of the exception occurring since in some cases we need to be
/// tolerant of error cases and still return some value
/// </summary>
public Func<Exception, object> ErrorHandler { get; set; }
/// <summary>
/// Gets or sets an event to signal when this queue item has been processed
/// </summary>

View File

@@ -77,6 +77,30 @@ namespace Microsoft.SqlTools.ServiceLayer
}
}
public static string AzureSqlDbEdition
{
get
{
return Keys.GetString(Keys.AzureSqlDbEdition);
}
}
public static string AzureSqlDwEdition
{
get
{
return Keys.GetString(Keys.AzureSqlDwEdition);
}
}
public static string AzureSqlStretchEdition
{
get
{
return Keys.GetString(Keys.AzureSqlStretchEdition);
}
}
public static string QueryServiceCancelAlreadyCompleted
{
get
@@ -3652,6 +3676,15 @@ namespace Microsoft.SqlTools.ServiceLayer
public const string ConnectionParamsValidateNullSqlAuth = "ConnectionParamsValidateNullSqlAuth";
public const string AzureSqlDbEdition = "AzureSqlDbEdition";
public const string AzureSqlDwEdition = "AzureSqlDwEdition";
public const string AzureSqlStretchEdition = "AzureSqlStretchEdition";
public const string QueryServiceCancelAlreadyCompleted = "QueryServiceCancelAlreadyCompleted";

View File

@@ -166,6 +166,18 @@
<comment>.
Parameters: 0 - component (string) </comment>
</data>
<data name="AzureSqlDbEdition" xml:space="preserve">
<value>Azure SQL DB</value>
<comment></comment>
</data>
<data name="AzureSqlDwEdition" xml:space="preserve">
<value>Azure SQL Data Warehouse</value>
<comment></comment>
</data>
<data name="AzureSqlStretchEdition" xml:space="preserve">
<value>Azure SQL Stretch Database</value>
<comment></comment>
</data>
<data name="QueryServiceCancelAlreadyCompleted" xml:space="preserve">
<value>The query has already completed, it cannot be cancelled</value>
<comment></comment>

View File

@@ -47,6 +47,11 @@ ConnectionParamsValidateNullServerName = ServerName cannot be null or empty
ConnectionParamsValidateNullSqlAuth(string component) = {0} cannot be null or empty when using SqlLogin authentication
### General connection service strings
AzureSqlDbEdition = Azure SQL DB
AzureSqlDwEdition = Azure SQL Data Warehouse
AzureSqlStretchEdition = Azure SQL Stretch Database
############################################################################
# Query Execution Service

View File

@@ -2280,6 +2280,21 @@
<target state="new">Never</target>
<note></note>
</trans-unit>
<trans-unit id="AzureSqlDbEdition">
<source>Azure SQL DB</source>
<target state="new">Azure SQL DB</target>
<note></note>
</trans-unit>
<trans-unit id="AzureSqlDwEdition">
<source>Azure SQL Data Warehouse</source>
<target state="new">Azure SQL Data Warehouse</target>
<note></note>
</trans-unit>
<trans-unit id="AzureSqlStretchEdition">
<source>Azure SQL Stretch Database</source>
<target state="new">Azure SQL Stretch Database</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -238,7 +238,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting
string script = string.Empty;
ScriptingObject scriptingObject = parameters.ScriptingObjects[0];
try
{
{
Server server = new Server(bindingContext.ServerConnection);
server.DefaultTextMode = true;
@@ -277,7 +277,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting
}
return null;
});
});
}
/// <summary>

View File

@@ -1,123 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="TestLocalizationConstant" xml:space="preserve">
<value>ES_LOCALIZATION</value>
</data>
</root>

View File

@@ -211,6 +211,7 @@ GO";
It.IsAny<string>(),
It.IsAny<Func<IBindingContext, CancellationToken, object>>(),
It.IsAny<Func<IBindingContext, object>>(),
It.IsAny<Func<Exception, object>>(),
It.IsAny<int?>(),
It.IsAny<int?>()))
.Callback<string, Func<IBindingContext, CancellationToken, object>, Func<IBindingContext, object>, int?, int?>(

View File

@@ -3,6 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Threading;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.SmoMetadataProvider;
@@ -133,6 +134,35 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
Assert.False(this.isCancelationRequested);
}
/// <summary>
/// Queues a single task
/// </summary>
[Fact]
public void QueueWithUnhandledExceptionTest()
{
InitializeTestSettings();
ManualResetEvent mre = new ManualResetEvent(false);
bool isExceptionHandled = false;
object defaultReturnObject = new object();
var queueItem = this.bindingQueue.QueueBindingOperation(
key: "testkey",
bindOperation: (context, CancellationToken) => throw new Exception("Unhandled!!"),
timeoutOperation: TestTimeoutOperation,
errorHandler: (exception) => {
isExceptionHandled = true;
mre.Set();
return defaultReturnObject;
});
mre.WaitOne(10000);
this.bindingQueue.StopQueueProcessor(15000);
Assert.True(isExceptionHandled);
var result = queueItem.GetResultAsT<object>();
Assert.Equal(defaultReturnObject, result);
}
/// <summary>
/// Queue a 100 short tasks
/// </summary>