Fixes BindingQueue, GetSignatureHelp and RefreshIntellisenseCache to be truly async (#2175)

This commit is contained in:
Cheena Malhotra
2023-08-23 13:46:13 -07:00
committed by GitHub
parent fcd84f242a
commit cea10c13e6
8 changed files with 433 additions and 414 deletions

View File

@@ -12,9 +12,8 @@ using System.Linq;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.Utility; using Microsoft.SqlTools.Utility;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{ {
@@ -130,7 +129,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
return context.IsConnected; return context.IsConnected;
} }
return false; return false;
} }
} }
/// <summary> /// <summary>
@@ -144,7 +143,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{ {
key = "disconnected_binding_context"; key = "disconnected_binding_context";
} }
lock (this.bindingContextLock) lock (this.bindingContextLock)
{ {
if (!this.BindingContextMap.ContainsKey(key)) if (!this.BindingContextMap.ContainsKey(key))
@@ -155,7 +154,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
} }
return this.BindingContextMap[key]; return this.BindingContextMap[key];
} }
} }
protected IEnumerable<IBindingContext> GetBindingContexts(string keyPrefix) protected IEnumerable<IBindingContext> GetBindingContexts(string keyPrefix)
@@ -269,10 +268,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
this.itemQueuedEvent, this.itemQueuedEvent,
token.WaitHandle token.WaitHandle
}; };
while (true) while (true)
{ {
// wait for with an item to be queued or the a cancellation request // wait for with an item to be queued or the cancellation request
WaitHandle.WaitAny(waitHandles); WaitHandle.WaitAny(waitHandles);
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
{ {
@@ -283,7 +282,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{ {
// dispatch all pending queue items // dispatch all pending queue items
while (this.HasPendingQueueItems) while (this.HasPendingQueueItems)
{ {
QueueItem queueItem = GetNextQueueItem(); QueueItem queueItem = GetNextQueueItem();
if (queueItem == null) if (queueItem == null)
{ {
@@ -291,7 +290,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
} }
IBindingContext bindingContext = GetOrCreateBindingContext(queueItem.Key); IBindingContext bindingContext = GetOrCreateBindingContext(queueItem.Key);
if (bindingContext == null) if (bindingContext == null)
{ {
queueItem.ItemProcessed.Set(); queueItem.ItemProcessed.Set();
continue; continue;
@@ -300,140 +299,16 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
var bindingContextTask = this.BindingContextTasks[bindingContext]; var bindingContextTask = this.BindingContextTasks[bindingContext];
// Run in the binding context task in case this task has to wait for a previous binding operation // Run in the binding context task in case this task has to wait for a previous binding operation
this.BindingContextTasks[bindingContext] = bindingContextTask.ContinueWith((task) => this.BindingContextTasks[bindingContext] = bindingContextTask.ContinueWith(
{ task => DispatchQueueItem(bindingContext, queueItem)
bool lockTaken = false; , TaskContinuationOptions.RunContinuationsAsynchronously);
try
{
// prefer the queue item binding item, otherwise use the context default timeout
int bindTimeout = queueItem.BindingTimeout ?? bindingContext.BindingTimeout;
// handle the case a previous binding operation is still running
if (!bindingContext.BindingLock.WaitOne(queueItem.WaitForLockTimeout ?? 0))
{
try
{
Logger.Warning("Binding queue operation timed out waiting for previous operation to finish");
queueItem.Result = queueItem.TimeoutOperation != null
? queueItem.TimeoutOperation(bindingContext)
: null;
}
catch (Exception ex)
{
Logger.Error("Exception running binding queue lock timeout handler: " + ex.ToString());
}
finally
{
queueItem.ItemProcessed.Set();
}
return;
}
bindingContext.BindingLock.Reset();
lockTaken = true;
// execute the binding operation
object result = null;
CancellationTokenSource cancelToken = new CancellationTokenSource();
// run the operation in a separate thread
var bindTask = Task.Run(() =>
{
try
{
result = queueItem.BindOperation(
bindingContext,
cancelToken.Token);
}
catch (Exception ex)
{
Logger.Error("Unexpected exception on the binding queue: " + ex.ToString());
if (queueItem.ErrorHandler != null)
{
try
{
result = queueItem.ErrorHandler(ex);
}
catch (Exception ex2)
{
Logger.Error("Unexpected exception in binding queue error handler: " + ex2.ToString());
}
}
if (IsExceptionOfType(ex, typeof(SqlException)) || IsExceptionOfType(ex, typeof(SocketException)))
{
if (this.OnUnhandledException != null)
{
this.OnUnhandledException(queueItem.Key, ex);
}
RemoveBindingContext(queueItem.Key);
}
}
});
Task.Run(() =>
{
try
{
// check if the binding tasks completed within the binding timeout
if (bindTask.Wait(bindTimeout))
{
queueItem.Result = result;
}
else
{
cancelToken.Cancel();
// if the task didn't complete then call the timeout callback
if (queueItem.TimeoutOperation != null)
{
queueItem.Result = queueItem.TimeoutOperation(bindingContext);
}
bindTask.ContinueWithOnFaulted(t => Logger.Error("Binding queue threw exception " + t.Exception.ToString()));
// Give the task a chance to complete before moving on to the next operation
bindTask.Wait();
}
}
catch (Exception ex)
{
Logger.Error("Binding queue task completion threw exception " + ex.ToString());
}
finally
{
// set item processed to avoid deadlocks
if (lockTaken)
{
bindingContext.BindingLock.Set();
}
queueItem.ItemProcessed.Set();
}
});
}
catch (Exception ex)
{
// catch and log any exceptions raised in the binding calls
// set item processed to avoid deadlocks
Logger.Error("Binding queue threw exception " + ex.ToString());
// set item processed to avoid deadlocks
if (lockTaken)
{
bindingContext.BindingLock.Set();
}
queueItem.ItemProcessed.Set();
}
}, TaskContinuationOptions.None);
// if a queue processing cancellation was requested then exit the loop // if a queue processing cancellation was requested then exit the loop
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
{ {
break; break;
} }
} }
} }
finally finally
{ {
@@ -450,6 +325,127 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
} }
} }
private void DispatchQueueItem(IBindingContext bindingContext, QueueItem queueItem)
{
bool lockTaken = false;
try
{
// prefer the queue item binding item, otherwise use the context default timeout - timeout is in milliseconds
int bindTimeoutInMs = queueItem.BindingTimeout ?? bindingContext.BindingTimeout;
// handle the case a previous binding operation is still running
if (!bindingContext.BindingLock.WaitOne(queueItem.WaitForLockTimeout ?? 0))
{
try
{
Logger.Warning("Binding queue operation timed out waiting for previous operation to finish");
queueItem.Result = queueItem.TimeoutOperation != null
? queueItem.TimeoutOperation(bindingContext)
: null;
}
catch (Exception ex)
{
Logger.Error("Exception running binding queue lock timeout handler: " + ex.ToString());
}
finally
{
queueItem.ItemProcessed.Set();
}
}
bindingContext.BindingLock.Reset();
lockTaken = true;
// execute the binding operation
object result = null;
CancellationTokenSource cancelToken = new CancellationTokenSource();
// run the operation in a separate thread
var bindTask = Task.Run(() =>
{
try
{
result = queueItem.BindOperation(
bindingContext,
cancelToken.Token);
}
catch (Exception ex)
{
Logger.Error("Unexpected exception on the binding queue: " + ex.ToString());
if (queueItem.ErrorHandler != null)
{
try
{
result = queueItem.ErrorHandler(ex);
}
catch (Exception ex2)
{
Logger.Error("Unexpected exception in binding queue error handler: " + ex2.ToString());
}
}
if (IsExceptionOfType(ex, typeof(SqlException)) || IsExceptionOfType(ex, typeof(SocketException)))
{
if (this.OnUnhandledException != null)
{
this.OnUnhandledException(queueItem.Key, ex);
}
RemoveBindingContext(queueItem.Key);
}
}
});
Task.Run(() =>
{
try
{
// check if the binding tasks completed within the binding timeout
if (bindTask.Wait(bindTimeoutInMs))
{
queueItem.Result = result;
}
else
{
cancelToken.Cancel();
// if the task didn't complete then call the timeout callback
if (queueItem.TimeoutOperation != null)
{
queueItem.Result = queueItem.TimeoutOperation(bindingContext);
}
bindTask.ContinueWithOnFaulted(t => Logger.Error("Binding queue threw exception " + t.Exception.ToString()));
// Give the task a chance to complete before moving on to the next operation
bindTask.Wait();
}
}
catch (Exception ex)
{
Logger.Error("Binding queue task completion threw exception " + ex.ToString());
}
finally
{
// set item processed to avoid deadlocks
if (lockTaken)
{
bindingContext.BindingLock.Set();
}
queueItem.ItemProcessed.Set();
}
});
}
catch (Exception ex)
{
// catch and log any exceptions raised in the binding calls
// set item processed to avoid deadlocks
Logger.Error("Binding queue threw exception " + ex.ToString());
// set item processed to avoid deadlocks
if (lockTaken)
{
bindingContext.BindingLock.Set();
}
queueItem.ItemProcessed.Set();
}
}
/// <summary> /// <summary>
/// Clear queued items /// Clear queued items
/// </summary> /// </summary>
@@ -487,7 +483,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
} }
} }
} }
private bool IsExceptionOfType(Exception ex, Type t) private bool IsExceptionOfType(Exception ex, Type t)
{ {
return ex.GetType() == t || (ex.InnerException != null && ex.InnerException.GetType() == t); return ex.GetType() == t || (ex.InnerException != null && ex.InnerException.GetType() == t);

View File

@@ -107,18 +107,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
compatibilityLevel: DatabaseCompatibilityLevel.Current, compatibilityLevel: DatabaseCompatibilityLevel.Current,
transactSqlVersion: TransactSqlVersion.Current); transactSqlVersion: TransactSqlVersion.Current);
private ConcurrentDictionary<string, bool> nonMssqlUriMap = new ConcurrentDictionary<string, bool>(); private ConcurrentDictionary<string, bool> nonMssqlUriMap = new();
private Lazy<Dictionary<string, ScriptParseInfo>> scriptParseInfoMap private Lazy<ConcurrentDictionary<string, ScriptParseInfo>> scriptParseInfoMap
= new Lazy<Dictionary<string, ScriptParseInfo>>(() => new Dictionary<string, ScriptParseInfo>()); = new Lazy<ConcurrentDictionary<string, ScriptParseInfo>>(() => new());
private readonly ConcurrentDictionary<string, ICompletionExtension> completionExtensions = new ConcurrentDictionary<string, ICompletionExtension>(); private readonly ConcurrentDictionary<string, ICompletionExtension> completionExtensions = new();
private readonly ConcurrentDictionary<string, DateTime> extAssemblyLastUpdateTime = new ConcurrentDictionary<string, DateTime>(); private readonly ConcurrentDictionary<string, DateTime> extAssemblyLastUpdateTime = new();
/// <summary> /// <summary>
/// Gets a mapping dictionary for SQL file URIs to ScriptParseInfo objects /// Gets a mapping dictionary for SQL file URIs to ScriptParseInfo objects
/// </summary> /// </summary>
internal Dictionary<string, ScriptParseInfo> ScriptParseInfoMap internal ConcurrentDictionary<string, ScriptParseInfo> ScriptParseInfoMap
{ {
get get
{ {
@@ -289,7 +289,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
WorkspaceServiceInstance.RegisterTextDocCloseCallback(HandleDidCloseTextDocumentNotification); WorkspaceServiceInstance.RegisterTextDocCloseCallback(HandleDidCloseTextDocumentNotification);
// Register a callback for when a connection is created // Register a callback for when a connection is created
ConnectionServiceInstance.RegisterOnConnectionTask(UpdateLanguageServiceOnConnection); ConnectionServiceInstance.RegisterOnConnectionTask(StartUpdateLanguageServiceOnConnection);
// Register a callback for when a connection is closed // Register a callback for when a connection is closed
ConnectionServiceInstance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference); ConnectionServiceInstance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference);
@@ -521,23 +521,6 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}; };
} }
// turn off this code until needed (10/28/2016)
#if false
private async Task HandleReferencesRequest(
ReferencesParams referencesParams,
RequestContext<Location[]> requestContext)
{
await requestContext.SendResult(null);
}
private async Task HandleDocumentHighlightRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<DocumentHighlight[]> requestContext)
{
await requestContext.SendResult(null);
}
#endif
internal async Task HandleSignatureHelpRequest( internal async Task HandleSignatureHelpRequest(
TextDocumentPosition textDocumentPosition, TextDocumentPosition textDocumentPosition,
RequestContext<SignatureHelp> requestContext) RequestContext<SignatureHelp> requestContext)
@@ -551,18 +534,26 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{ {
ScriptFile scriptFile = CurrentWorkspace.GetFile( ScriptFile scriptFile = CurrentWorkspace.GetFile(
textDocumentPosition.TextDocument.Uri); textDocumentPosition.TextDocument.Uri);
SignatureHelp help = null;
if (scriptFile != null) if (scriptFile != null)
{ {
help = GetSignatureHelp(textDocumentPosition, scriptFile); // Start task asynchronously without blocking main thread - this is by design.
} // Explanation: STS message queues are single-threaded queues, which should be unblocked as soon as possible.
if (help != null) // All Long-running tasks should be performed in a non-blocking background task, and results should be sent when ready.
{ #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
await requestContext.SendResult(help); GetSignatureHelp(textDocumentPosition, scriptFile)
} .ContinueWith(async task =>
else {
{ var result = await task;
await requestContext.SendResult(new SignatureHelp()); if (result != null)
{
await requestContext.SendResult(result);
}
else
{
await requestContext.SendResult(new SignatureHelp());
}
});
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
} }
} }
} }
@@ -683,9 +674,33 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <summary> /// <summary>
/// Handle the rebuild IntelliSense cache notification /// Handle the rebuild IntelliSense cache notification
/// </summary> /// </summary>
public async Task HandleRebuildIntelliSenseNotification( /// <param name="rebuildParams">Rebuild params</param>
/// <param name="eventContext">Event context</param>
/// <returns>Async task</returns>
public Task HandleRebuildIntelliSenseNotification(
RebuildIntelliSenseParams rebuildParams, RebuildIntelliSenseParams rebuildParams,
EventContext eventContext) EventContext eventContext)
{
// Start task asynchronously without blocking main thread - this is by design.
// Explanation: STS message queues are single-threaded queues, which should be unblocked as soon as possible.
// All Long-running tasks should be performed in a non-blocking background task, and results should be sent when ready.
Task.Factory.StartNew(async () =>
{
await DoHandleRebuildIntellisenseNotification(rebuildParams, eventContext);
}, CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.Default);
return Task.CompletedTask;
}
/// <summary>
/// Internal method to handle rebuild intellisense notification
/// </summary>
/// <param name="rebuildParams">Rebuild params</param>
/// <param name="eventContext">Event context</param>
/// <returns>Async task</returns>
public async Task DoHandleRebuildIntellisenseNotification(RebuildIntelliSenseParams rebuildParams, EventContext eventContext)
{ {
try try
{ {
@@ -694,7 +709,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// This URI doesn't come in escaped - so if it's a file path with reserved characters (such as %) // This URI doesn't come in escaped - so if it's a file path with reserved characters (such as %)
// then we'll fail to find it since GetFile expects the URI to be a fully-escaped URI as that's // then we'll fail to find it since GetFile expects the URI to be a fully-escaped URI as that's
// what the document events are sent in as. // what the document events are sent in as.
var escapedOwnerUri = Uri.EscapeUriString(rebuildParams.OwnerUri); var escapedOwnerUri = Uri.EscapeDataString(rebuildParams.OwnerUri);
// Skip closing this file if the file doesn't exist // Skip closing this file if the file doesn't exist
var scriptFile = this.CurrentWorkspace.GetFile(escapedOwnerUri); var scriptFile = this.CurrentWorkspace.GetFile(escapedOwnerUri);
if (scriptFile == null) if (scriptFile == null)
@@ -710,47 +725,44 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// check that there is an active connection for the current editor // check that there is an active connection for the current editor
if (connInfo != null) if (connInfo != null)
{ {
await Task.Run(() => // Get the current ScriptInfo if one exists so we can lock it while we're rebuilding the cache
ScriptParseInfo scriptInfo = GetScriptParseInfo(connInfo.OwnerUri, createIfNotExists: false);
if (scriptInfo != null && scriptInfo.IsConnected &&
Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout))
{ {
// Get the current ScriptInfo if one exists so we can lock it while we're rebuilding the cache try
ScriptParseInfo scriptInfo = GetScriptParseInfo(connInfo.OwnerUri, createIfNotExists: false);
if (scriptInfo != null && scriptInfo.IsConnected &&
Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout))
{ {
try this.BindingQueue.AddConnectionContext(connInfo, featureName: Constants.LanguageServiceFeature, overwrite: true);
{ RemoveScriptParseInfo(rebuildParams.OwnerUri);
this.BindingQueue.AddConnectionContext(connInfo, featureName: Constants.LanguageServiceFeature, overwrite: true); await UpdateLanguageServiceOnConnection(connInfo);
RemoveScriptParseInfo(rebuildParams.OwnerUri); }
UpdateLanguageServiceOnConnection(connInfo).Wait(); catch (Exception ex)
} {
catch (Exception ex) Logger.Error("Unknown error " + ex.ToString());
{ }
Logger.Error("Unknown error " + ex.ToString()); finally
} {
finally // Set Metadata Build event to Signal state.
{ Monitor.Exit(scriptInfo.BuildingMetadataLock);
// Set Metadata Build event to Signal state.
Monitor.Exit(scriptInfo.BuildingMetadataLock);
}
} }
// if not in the preview window and diagnostics are enabled then run diagnostics // if not in the preview window and diagnostics are enabled then run diagnostics
if (!IsPreviewWindow(scriptFile) if (!IsPreviewWindow(scriptFile)
&& CurrentWorkspaceSettings.IsDiagnosticsEnabled) && CurrentWorkspaceSettings.IsDiagnosticsEnabled)
{ {
RunScriptDiagnostics( await RunScriptDiagnostics(
new ScriptFile[] { scriptFile }, new ScriptFile[] { scriptFile },
eventContext); eventContext);
} }
// Send a notification to signal that autocomplete is ready // Send a notification to signal that autocomplete is ready
ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = connInfo.OwnerUri }); await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = connInfo.OwnerUri });
}); }
} else
else {
{ // Send a notification to signal that autocomplete is ready
// Send a notification to signal that autocomplete is ready await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = rebuildParams.OwnerUri });
await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = rebuildParams.OwnerUri }); }
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -887,121 +899,125 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <param name="filePath"></param> /// <param name="filePath"></param>
/// <param name="sqlText"></param> /// <param name="sqlText"></param>
/// <returns>The ParseResult instance returned from SQL Parser</returns> /// <returns>The ParseResult instance returned from SQL Parser</returns>
public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo) public Task<ParseResult> ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo)
{ {
Logger.Verbose($"ParseAndBind - {scriptFile}"); Logger.Verbose($"ParseAndBind - {scriptFile}");
// get or create the current parse info object // get or create the current parse info object
ScriptParseInfo parseInfo = GetScriptParseInfo(scriptFile.ClientUri, createIfNotExists: true); ScriptParseInfo parseInfo = GetScriptParseInfo(scriptFile.ClientUri, createIfNotExists: true);
return Task.Run(() =>
if (Monitor.TryEnter(parseInfo.BuildingMetadataLock, LanguageService.BindingTimeout))
{ {
try if (Monitor.TryEnter(parseInfo.BuildingMetadataLock, LanguageService.BindingTimeout))
{ {
if (connInfo == null || !parseInfo.IsConnected) try
{ {
// parse on separate thread so stack size can be increased if (connInfo == null || !parseInfo.IsConnected)
var parseThread = new Thread(() =>
{ {
try // parse on separate thread so stack size can be increased
{ var parseThread = new Thread(() =>
// parse current SQL file contents to retrieve a list of errors
ParseResult parseResult = Parser.IncrementalParse(
scriptFile.Contents,
parseInfo.ParseResult,
this.DefaultParseOptions);
parseInfo.ParseResult = parseResult;
}
catch (Exception e)
{
// Log the exception but don't rethrow it to prevent parsing errors from crashing SQL Tools Service
Logger.Error(string.Format("An unexpected error occured while parsing: {0}", e.ToString()));
}
}, ConnectedBindingQueue.QueueThreadStackSize);
parseThread.Start();
parseThread.Join();
}
else
{
QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
key: parseInfo.ConnectionKey,
bindingTimeout: LanguageService.BindingTimeout,
bindOperation: (bindingContext, cancelToken) =>
{ {
try try
{ {
// parse current SQL file contents to retrieve a list of errors
ParseResult parseResult = Parser.IncrementalParse( ParseResult parseResult = Parser.IncrementalParse(
scriptFile.Contents, scriptFile.Contents,
parseInfo.ParseResult, parseInfo.ParseResult,
bindingContext.ParseOptions); this.DefaultParseOptions);
parseInfo.ParseResult = parseResult; parseInfo.ParseResult = parseResult;
}
List<ParseResult> parseResults = new List<ParseResult>(); catch (Exception e)
parseResults.Add(parseResult); {
if (bindingContext.IsConnected && bindingContext.Binder != null) // Log the exception but don't rethrow it to prevent parsing errors from crashing SQL Tools Service
Logger.Error(string.Format("An unexpected error occured while parsing: {0}", e.ToString()));
}
}, ConnectedBindingQueue.QueueThreadStackSize);
parseThread.Start();
parseThread.Join();
}
else
{
QueueItem queueItem = this.BindingQueue.QueueBindingOperation(
key: parseInfo.ConnectionKey,
bindingTimeout: LanguageService.BindingTimeout,
bindOperation: (bindingContext, cancelToken) =>
{
try
{ {
bindingContext.Binder.Bind( ParseResult parseResult = Parser.IncrementalParse(
parseResults, scriptFile.Contents,
connInfo.ConnectionDetails.DatabaseName, parseInfo.ParseResult,
BindMode.Batch); bindingContext.ParseOptions);
parseInfo.ParseResult = parseResult;
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
if (bindingContext.IsConnected && bindingContext.Binder != null)
{
bindingContext.Binder.Bind(
parseResults,
connInfo.ConnectionDetails.DatabaseName,
BindMode.Batch);
}
}
catch (ConnectionException)
{
Logger.Error("Hit connection exception while binding - disposing binder object...");
}
catch (SqlParserInternalBinderError)
{
Logger.Error("Hit connection exception while binding - disposing binder object...");
}
catch (Exception ex)
{
Logger.Error("Unknown exception during parsing " + ex.ToString());
} }
}
catch (ConnectionException)
{
Logger.Error("Hit connection exception while binding - disposing binder object...");
}
catch (SqlParserInternalBinderError)
{
Logger.Error("Hit connection exception while binding - disposing binder object...");
}
catch (Exception ex)
{
Logger.Error("Unknown exception during parsing " + ex.ToString());
}
return null; return null;
}); });
queueItem.ItemProcessed.WaitOne(); queueItem.ItemProcessed.WaitOne();
}
}
catch (Exception ex)
{
// reset the parse result to do a full parse next time
parseInfo.ParseResult = null;
Logger.Error("Unknown exception during parsing " + ex.ToString());
}
finally
{
Monitor.Exit(parseInfo.BuildingMetadataLock);
} }
} }
catch (Exception ex) else
{ {
// reset the parse result to do a full parse next time Logger.Warning("Binding metadata lock timeout in ParseAndBind");
parseInfo.ParseResult = null;
Logger.Error("Unknown exception during parsing " + ex.ToString());
} }
finally
{
Monitor.Exit(parseInfo.BuildingMetadataLock);
}
}
else
{
Logger.Warning("Binding metadata lock timeout in ParseAndBind");
}
return parseInfo.ParseResult; return parseInfo.ParseResult;
});
}
/// <summary>
/// Runs UpdateLanguageServiceOnConnection as a background task
/// </summary>
/// <param name="info">Connection Info</param>
/// <returns></returns>
public Task StartUpdateLanguageServiceOnConnection(ConnectionInfo info)
{
// Start task asynchronously without blocking main thread - this is by design.
// Explanation: STS message queues are single-threaded queues, which should be unblocked as soon as possible.
// All Long-running tasks should be performed in a non-blocking background task, and results should be sent when ready.
Task.Factory.StartNew(() => UpdateLanguageServiceOnConnection(info));
return Task.CompletedTask;
} }
/// <summary> /// <summary>
/// Starts a Task to update the autocomplete metadata provider when the user connects to a database /// Starts a Task to update the autocomplete metadata provider when the user connects to a database
/// </summary> /// </summary>
/// <param name="info">Connection info</param> /// <param name="info"></param>
public Task UpdateLanguageServiceOnConnection(ConnectionInfo info) public async Task UpdateLanguageServiceOnConnection(ConnectionInfo info)
{
return Task.Run(() =>
{
DoUpdateLanguageServiceOnConnection(info);
});
}
/// <summary>
/// Update the autocomplete metadata provider when the user connects to a database synchronously
/// </summary>
/// <param name="info">Connection info</param>
public void DoUpdateLanguageServiceOnConnection(ConnectionInfo info)
{ {
if (ConnectionService.IsDedicatedAdminConnection(info.ConnectionDetails)) if (ConnectionService.IsDedicatedAdminConnection(info.ConnectionDetails))
{ {
@@ -1028,10 +1044,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
Monitor.Exit(scriptInfo.BuildingMetadataLock); Monitor.Exit(scriptInfo.BuildingMetadataLock);
} }
} }
PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue); await PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue).ContinueWith(async _ =>
{
// Send a notification to signal that autocomplete is ready // Send a notification to signal that autocomplete is ready
ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = info.OwnerUri }); await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = info.OwnerUri });
});
} }
@@ -1043,7 +1060,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// </summary> /// </summary>
/// <param name="info"></param> /// <param name="info"></param>
/// <param name="scriptInfo"></param> /// <param name="scriptInfo"></param>
internal void PrepopulateCommonMetadata( internal async Task PrepopulateCommonMetadata(
ConnectionInfo info, ConnectionInfo info,
ScriptParseInfo scriptInfo, ScriptParseInfo scriptInfo,
ConnectedBindingQueue bindingQueue) ConnectedBindingQueue bindingQueue)
@@ -1060,71 +1077,73 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
return; return;
} }
ParseAndBind(scriptFile, info); await ParseAndBind(scriptFile, info).ContinueWith(t =>
if (Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout))
{ {
try if (Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout))
{ {
QueueItem queueItem = bindingQueue.QueueBindingOperation( try
key: scriptInfo.ConnectionKey, {
bindingTimeout: PrepopulateBindTimeout, QueueItem queueItem = bindingQueue.QueueBindingOperation(
waitForLockTimeout: PrepopulateBindTimeout, key: scriptInfo.ConnectionKey,
bindOperation: (bindingContext, cancelToken) => bindingTimeout: PrepopulateBindTimeout,
{ waitForLockTimeout: PrepopulateBindTimeout,
// parse a simple statement that returns common metadata bindOperation: (bindingContext, cancelToken) =>
ParseResult parseResult = Parser.Parse(
"select ",
bindingContext.ParseOptions);
if (bindingContext.IsConnected && bindingContext.Binder != null)
{ {
List<ParseResult> parseResults = new List<ParseResult>(); // parse a simple statement that returns common metadata
parseResults.Add(parseResult); ParseResult parseResult = Parser.Parse(
bindingContext.Binder.Bind( "select ",
parseResults,
info.ConnectionDetails.DatabaseName,
BindMode.Batch);
// 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);
parseResult = Parser.Parse(
"exec ",
bindingContext.ParseOptions); 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);
parseResults = new List<ParseResult>(); // get the completion list from SQL Parser
parseResults.Add(parseResult); var suggestions = Resolver.FindCompletions(
bindingContext.Binder.Bind( parseResult, 1, 8,
parseResults, bindingContext.MetadataDisplayInfoProvider);
info.ConnectionDetails.DatabaseName,
BindMode.Batch);
// get the completion list from SQL Parser // this forces lazy evaluation of the suggestion metadata
suggestions = Resolver.FindCompletions( AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8);
parseResult, 1, 6,
bindingContext.MetadataDisplayInfoProvider);
// this forces lazy evaluation of the suggestion metadata parseResult = Parser.Parse(
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6); "exec ",
} bindingContext.ParseOptions);
return null;
});
queueItem.ItemProcessed.WaitOne(); 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);
// this forces lazy evaluation of the suggestion metadata
AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 6, 6);
}
return null;
});
queueItem.ItemProcessed.WaitOne();
}
catch (Exception ex)
{
Logger.Error("Exception in PrepopulateCommonMetadata " + ex.ToString());
}
finally
{
Monitor.Exit(scriptInfo.BuildingMetadataLock);
}
} }
catch });
{
}
finally
{
Monitor.Exit(scriptInfo.BuildingMetadataLock);
}
}
} }
} }
@@ -1348,7 +1367,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
if (RequiresReparse(scriptParseInfo, scriptFile)) if (RequiresReparse(scriptParseInfo, scriptFile))
{ {
scriptParseInfo.ParseResult = ParseAndBind(scriptFile, connInfo); scriptParseInfo.ParseResult = ParseAndBind(scriptFile, connInfo).GetAwaiter().GetResult();
} }
// Get token from selected text // Get token from selected text
@@ -1499,7 +1518,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// <summary> /// <summary>
/// Get function signature help for the current position /// Get function signature help for the current position
/// </summary> /// </summary>
internal SignatureHelp? GetSignatureHelp(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile) internal async Task<SignatureHelp?> GetSignatureHelp(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile)
{ {
Logger.Verbose($"GetSignatureHelp - {scriptFile}"); Logger.Verbose($"GetSignatureHelp - {scriptFile}");
int startLine = textDocumentPosition.Position.Line; int startLine = textDocumentPosition.Position.Line;
@@ -1522,8 +1541,9 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// reparse and bind the SQL statement if needed // reparse and bind the SQL statement if needed
if (RequiresReparse(scriptParseInfo, scriptFile)) if (RequiresReparse(scriptParseInfo, scriptFile))
{ {
ParseAndBind(scriptFile, connInfo); await ParseAndBind(scriptFile, connInfo);
} else }
else
{ {
Logger.Verbose($"GetSignatureHelp - No reparse needed for {scriptFile}"); Logger.Verbose($"GetSignatureHelp - No reparse needed for {scriptFile}");
} }
@@ -1570,6 +1590,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
Logger.Verbose($"GetSignatureHelp - Got result {queueItem.Result}"); Logger.Verbose($"GetSignatureHelp - Got result {queueItem.Result}");
return queueItem.GetResultAsT<SignatureHelp>(); return queueItem.GetResultAsT<SignatureHelp>();
} }
catch (Exception ex)
{
Logger.Error("Exception in GetSignatureHelp " + ex.ToString());
}
finally finally
{ {
Monitor.Exit(scriptParseInfo.BuildingMetadataLock); Monitor.Exit(scriptParseInfo.BuildingMetadataLock);
@@ -1615,7 +1639,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// reparse and bind the SQL statement if needed // reparse and bind the SQL statement if needed
if (RequiresReparse(scriptParseInfo, scriptFile)) if (RequiresReparse(scriptParseInfo, scriptFile))
{ {
ParseAndBind(scriptFile, connInfo); await ParseAndBind(scriptFile, connInfo);
} }
// if the parse failed then return the default list // if the parse failed then return the default list
@@ -1693,14 +1717,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// Gets a list of semantic diagnostic marks for the provided script file /// Gets a list of semantic diagnostic marks for the provided script file
/// </summary> /// </summary>
/// <param name="scriptFile"></param> /// <param name="scriptFile"></param>
internal ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile) internal async Task<ScriptFileMarker[]> GetSemanticMarkers(ScriptFile scriptFile)
{ {
ConnectionInfo connInfo; ConnectionInfo connInfo;
ConnectionServiceInstance.TryFindConnection( ConnectionServiceInstance.TryFindConnection(
scriptFile.ClientUri, scriptFile.ClientUri,
out connInfo); out connInfo);
var parseResult = ParseAndBind(scriptFile, connInfo); var parseResult = await ParseAndBind(scriptFile, connInfo);
// build a list of SQL script file markers from the errors // build a list of SQL script file markers from the errors
List<ScriptFileMarker> markers = new List<ScriptFileMarker>(); List<ScriptFileMarker> markers = new List<ScriptFileMarker>();
@@ -1835,10 +1859,18 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
} }
Logger.Verbose("Analyzing script file: " + scriptFile.FilePath); Logger.Verbose("Analyzing script file: " + scriptFile.FilePath);
ScriptFileMarker[] semanticMarkers = GetSemanticMarkers(scriptFile);
Logger.Verbose("Analysis complete.");
await DiagnosticsHelper.PublishScriptDiagnostics(scriptFile, semanticMarkers, eventContext); // Start task asynchronously without blocking main thread - this is by design.
// Explanation: STS message queues are single-threaded queues, which should be unblocked as soon as possible.
// All Long-running tasks should be performed in a non-blocking background task, and results should be sent when ready.
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
GetSemanticMarkers(scriptFile).ContinueWith(async t =>
{
var semanticMarkers = t.GetAwaiter().GetResult();
Logger.Verbose("Analysis complete.");
await DiagnosticsHelper.PublishScriptDiagnostics(scriptFile, semanticMarkers, eventContext);
}, CancellationToken.None);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
} }
} }
@@ -1861,7 +1893,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
else else
{ {
Logger.Verbose($"Adding ScriptParseInfo for uri {uri}"); Logger.Verbose($"Adding ScriptParseInfo for uri {uri}");
this.ScriptParseInfoMap.Add(uri, scriptInfo); this.ScriptParseInfoMap.TryAdd(uri, scriptInfo);
} }
} }
@@ -1887,7 +1919,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
Logger.Verbose($"ScriptParseInfo for uri {uri} did not exist, creating new one"); Logger.Verbose($"ScriptParseInfo for uri {uri} did not exist, creating new one");
// create a new script parse info object and initialize with the current settings // create a new script parse info object and initialize with the current settings
ScriptParseInfo scriptInfo = new ScriptParseInfo(); ScriptParseInfo scriptInfo = new ScriptParseInfo();
this.ScriptParseInfoMap.Add(uri, scriptInfo); this.ScriptParseInfoMap.TryAdd(uri, scriptInfo);
return scriptInfo; return scriptInfo;
} }
else else
@@ -1902,15 +1934,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{ {
lock (this.parseMapLock) lock (this.parseMapLock)
{ {
if (this.ScriptParseInfoMap.ContainsKey(uri)) Logger.Verbose($"Removing ScriptParseInfo for uri {uri}");
{ return this.ScriptParseInfoMap.TryRemove(uri, out _);
Logger.Verbose($"Removing ScriptParseInfo for uri {uri}");
return this.ScriptParseInfoMap.Remove(uri);
}
else
{
return false;
}
} }
} }

View File

@@ -1064,22 +1064,19 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
// what the document events are sent in as. // what the document events are sent in as.
var escapedOwnerUri = Uri.EscapeUriString(request.OwnerUri); var escapedOwnerUri = Uri.EscapeUriString(request.OwnerUri);
// If it is a document selection, we'll retrieve the text from the document // If it is a document selection, we'll retrieve the text from the document
ExecuteDocumentSelectionParams docRequest = request as ExecuteDocumentSelectionParams; if (request is ExecuteDocumentSelectionParams docRequest)
if (docRequest != null)
{ {
return GetSqlTextFromSelectionData(escapedOwnerUri, docRequest.QuerySelection); return GetSqlTextFromSelectionData(escapedOwnerUri, docRequest.QuerySelection);
} }
// If it is a document statement, we'll retrieve the text from the document // If it is a document statement, we'll retrieve the text from the document
ExecuteDocumentStatementParams stmtRequest = request as ExecuteDocumentStatementParams; if (request is ExecuteDocumentStatementParams stmtRequest)
if (stmtRequest != null)
{ {
return GetSqlStatementAtPosition(escapedOwnerUri, stmtRequest.Line, stmtRequest.Column); return GetSqlStatementAtPosition(escapedOwnerUri, stmtRequest.Line, stmtRequest.Column);
} }
// If it is an ExecuteStringParams, return the text as is // If it is an ExecuteStringParams, return the text as is
ExecuteStringParams stringRequest = request as ExecuteStringParams; if (request is ExecuteStringParams stringRequest)
if (stringRequest != null)
{ {
return stringRequest.Query; return stringRequest.Query;
} }

View File

@@ -15,6 +15,7 @@ using Microsoft.SqlTools.Utility;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.ServiceLayer.Utility;
using System.Collections.Concurrent;
namespace Microsoft.SqlTools.ServiceLayer.Workspace namespace Microsoft.SqlTools.ServiceLayer.Workspace
{ {
@@ -35,7 +36,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace
"vscode-notebook-cell" "vscode-notebook-cell"
}; };
private Dictionary<string, ScriptFile> workspaceFiles = new Dictionary<string, ScriptFile>(); private ConcurrentDictionary<string, ScriptFile> workspaceFiles = new ConcurrentDictionary<string, ScriptFile>();
#endregion #endregion
@@ -121,7 +122,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace
{ {
scriptFile = new ScriptFile(resolvedFile.FilePath, resolvedFile.ClientUri,streamReader); scriptFile = new ScriptFile(resolvedFile.FilePath, resolvedFile.ClientUri,streamReader);
this.workspaceFiles.Add(keyName, scriptFile); this.workspaceFiles.TryAdd(keyName, scriptFile);
} }
Logger.Verbose("Opened file on disk: " + resolvedFile.FilePath); Logger.Verbose("Opened file on disk: " + resolvedFile.FilePath);
@@ -235,7 +236,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace
{ {
scriptFile = new ScriptFile(resolvedFile.FilePath, resolvedFile.ClientUri, initialBuffer); scriptFile = new ScriptFile(resolvedFile.FilePath, resolvedFile.ClientUri, initialBuffer);
this.workspaceFiles.Add(keyName, scriptFile); this.workspaceFiles.TryAdd(keyName, scriptFile);
Logger.Verbose("Opened file as in-memory buffer: " + resolvedFile.FilePath); Logger.Verbose("Opened file as in-memory buffer: " + resolvedFile.FilePath);
} }
@@ -260,7 +261,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace
{ {
Validate.IsNotNull("scriptFile", scriptFile); Validate.IsNotNull("scriptFile", scriptFile);
this.workspaceFiles.Remove(scriptFile.Id); this.workspaceFiles.TryRemove(scriptFile.Id, out _);
} }
internal string GetBaseFilePath(string filePath) internal string GetBaseFilePath(string filePath)

View File

@@ -177,7 +177,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer
requestContext.Verify(x => x.SendError(It.IsAny<string>(), 0, It.IsAny<string>()), Times.Once); requestContext.Verify(x => x.SendError(It.IsAny<string>(), 0, It.IsAny<string>()), Times.Once);
ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true }; ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true };
autoCompleteService.ParseAndBind(result.ScriptFile, result.ConnectionInfo); await autoCompleteService.ParseAndBind(result.ScriptFile, result.ConnectionInfo);
scriptInfo.ConnectionKey = autoCompleteService.BindingQueue.AddConnectionContext(result.ConnectionInfo); scriptInfo.ConnectionKey = autoCompleteService.BindingQueue.AddConnectionContext(result.ConnectionInfo);
//Invoke auto completion with extension enabled //Invoke auto completion with extension enabled
@@ -219,7 +219,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer
/// provide signature help. /// provide signature help.
/// </summary> /// </summary>
[Test] [Test]
public void GetSignatureHelpReturnsNotNullIfParseInfoInitialized() public async Task GetSignatureHelpReturnsNotNullIfParseInfoInitialized()
{ {
// When we make a connection to a live database // When we make a connection to a live database
Hosting.ServiceHost.SendEventIgnoreExceptions = true; Hosting.ServiceHost.SendEventIgnoreExceptions = true;
@@ -243,14 +243,14 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer
// If the SQL has already been parsed // If the SQL has already been parsed
var service = CreateLanguageService(result.ScriptFile); var service = CreateLanguageService(result.ScriptFile);
service.DoUpdateLanguageServiceOnConnection(result.ConnectionInfo); await service.UpdateLanguageServiceOnConnection(result.ConnectionInfo);
// We should get back a non-null ScriptParseInfo // We should get back a non-null ScriptParseInfo
ScriptParseInfo? parseInfo = service.GetScriptParseInfo(result.ScriptFile.ClientUri); ScriptParseInfo? parseInfo = service.GetScriptParseInfo(result.ScriptFile.ClientUri);
Assert.That(parseInfo, Is.Not.Null, "ScriptParseInfo"); Assert.That(parseInfo, Is.Not.Null, "ScriptParseInfo");
// And we should get back a non-null SignatureHelp // And we should get back a non-null SignatureHelp
SignatureHelp? signatureHelp = service.GetSignatureHelp(textDocument, result.ScriptFile); SignatureHelp? signatureHelp = await service.GetSignatureHelp(textDocument, result.ScriptFile);
Assert.That(signatureHelp, Is.Not.Null, "SignatureHelp"); Assert.That(signatureHelp, Is.Not.Null, "SignatureHelp");
} }
@@ -319,7 +319,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer
testDb.RunQuery("CREATE TABLE dbo.foo(col1 int)"); testDb.RunQuery("CREATE TABLE dbo.foo(col1 int)");
// And refresh the cache // And refresh the cache
await langService.HandleRebuildIntelliSenseNotification( await langService.DoHandleRebuildIntellisenseNotification(
new RebuildIntelliSenseParams() { OwnerUri = connectionInfoResult.ScriptFile.ClientUri }, new RebuildIntelliSenseParams() { OwnerUri = connectionInfoResult.ScriptFile.ClientUri },
new TestEventContext()); new TestEventContext());
@@ -446,7 +446,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer
testDb.RunQuery(createTableQueries); testDb.RunQuery(createTableQueries);
// And refresh the cache // And refresh the cache
await langService.HandleRebuildIntelliSenseNotification( await langService.DoHandleRebuildIntellisenseNotification(
new RebuildIntelliSenseParams() { OwnerUri = connectionInfoResult.ScriptFile.ClientUri }, new RebuildIntelliSenseParams() { OwnerUri = connectionInfoResult.ScriptFile.ClientUri },
new TestEventContext()); new TestEventContext());

View File

@@ -259,7 +259,7 @@ GO";
scriptFile.Contents = "select * from dbo.func ()"; scriptFile.Contents = "select * from dbo.func ()";
ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true }; ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true };
languageService.ScriptParseInfoMap.Add(scriptFile.ClientUri, scriptInfo); languageService.ScriptParseInfoMap.TryAdd(scriptFile.ClientUri, scriptInfo);
// Pass in null connection info to force doing a local parse since that hits the BindingQueue timeout // Pass in null connection info to force doing a local parse since that hits the BindingQueue timeout
// before we want it to (this is testing the timeout trying to fetch the definitions after the parse) // before we want it to (this is testing the timeout trying to fetch the definitions after the parse)
@@ -729,9 +729,9 @@ GO";
Thread.Sleep(2000); Thread.Sleep(2000);
ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true }; ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true };
service.ParseAndBind(scriptFile, connInfo); await service.ParseAndBind(scriptFile, connInfo);
scriptInfo.ConnectionKey = bindingQueue.AddConnectionContext(connInfo); scriptInfo.ConnectionKey = bindingQueue.AddConnectionContext(connInfo);
service.ScriptParseInfoMap.Add(OwnerUri, scriptInfo); service.ScriptParseInfoMap.TryAdd(OwnerUri, scriptInfo);
// When I call the language service // When I call the language service
var objectResult = service.GetDefinition(objectDocument, scriptFile, connInfo); var objectResult = service.GetDefinition(objectDocument, scriptFile, connInfo);
@@ -750,7 +750,7 @@ GO";
Cleanup(objectResult.Locations); Cleanup(objectResult.Locations);
Cleanup(sysResult.Locations); Cleanup(sysResult.Locations);
Cleanup(masterResult.Locations); Cleanup(masterResult.Locations);
service.ScriptParseInfoMap.Remove(OwnerUri); service.ScriptParseInfoMap.TryRemove(OwnerUri, out _);
connInfo.RemoveAllConnections(); connInfo.RemoveAllConnections();
} }
@@ -786,9 +786,9 @@ GO";
Thread.Sleep(2000); Thread.Sleep(2000);
ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true }; ScriptParseInfo scriptInfo = new ScriptParseInfo { IsConnected = true };
service.ParseAndBind(scriptFile, connInfo); await service.ParseAndBind(scriptFile, connInfo);
scriptInfo.ConnectionKey = bindingQueue.AddConnectionContext(connInfo); scriptInfo.ConnectionKey = bindingQueue.AddConnectionContext(connInfo);
service.ScriptParseInfoMap.Add(TestUri, scriptInfo); service.ScriptParseInfoMap.TryAdd(TestUri, scriptInfo);
// When I call the language service // When I call the language service
var fnResult = service.GetDefinition(fnDocument, scriptFile, connInfo); var fnResult = service.GetDefinition(fnDocument, scriptFile, connInfo);
@@ -807,7 +807,7 @@ GO";
Cleanup(fnResult.Locations); Cleanup(fnResult.Locations);
Cleanup(sysResult.Locations); Cleanup(sysResult.Locations);
Cleanup(masterResult.Locations); Cleanup(masterResult.Locations);
service.ScriptParseInfoMap.Remove(TestUri); service.ScriptParseInfoMap.TryRemove(TestUri, out _);
connInfo.RemoveAllConnections(); connInfo.RemoveAllConnections();
} }

View File

@@ -218,7 +218,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
}; };
ParseResult parseResult = langService.ParseAndBind(scriptFile, null); ParseResult parseResult = langService.ParseAndBind(scriptFile, null).GetAwaiter().GetResult();
ScriptParseInfo scriptParseInfo = langService.GetScriptParseInfo(scriptFile.ClientUri, true); ScriptParseInfo scriptParseInfo = langService.GetScriptParseInfo(scriptFile.ClientUri, true);
return new ScriptDocumentInfo(textDocumentPosition, scriptFile, scriptParseInfo); return new ScriptDocumentInfo(textDocumentPosition, scriptFile, scriptParseInfo);

View File

@@ -26,17 +26,17 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
public void LatestSqlParserIsUsedByDefault() public void LatestSqlParserIsUsedByDefault()
{ {
// This should only parse correctly on SQL server 2016 or newer // This should only parse correctly on SQL server 2016 or newer
const string sql2016Text = const string sql2016Text =
@"CREATE SECURITY POLICY [FederatedSecurityPolicy]" + "\r\n" + @"CREATE SECURITY POLICY [FederatedSecurityPolicy]" + "\r\n" +
@"ADD FILTER PREDICATE [rls].[fn_securitypredicate]([CustomerId])" + "\r\n" + @"ADD FILTER PREDICATE [rls].[fn_securitypredicate]([CustomerId])" + "\r\n" +
@"ON [dbo].[Customer];"; @"ON [dbo].[Customer];";
LanguageService service = TestObjects.GetTestLanguageService(); LanguageService service = TestObjects.GetTestLanguageService();
// parse // parse
var scriptFile = new ScriptFile(); var scriptFile = new ScriptFile();
scriptFile.SetFileContents(sql2016Text); scriptFile.SetFileContents(sql2016Text);
ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile).GetAwaiter().GetResult();
// verify that no errors are detected // verify that no errors are detected
Assert.AreEqual(0, fileMarkers.Length); Assert.AreEqual(0, fileMarkers.Length);
@@ -57,7 +57,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
// parse the sql statement // parse the sql statement
var scriptFile = new ScriptFile(); var scriptFile = new ScriptFile();
scriptFile.SetFileContents(sqlWithErrors); scriptFile.SetFileContents(sqlWithErrors);
ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile).GetAwaiter().GetResult();
// verify there are no errors // verify there are no errors
Assert.AreEqual(0, fileMarkers.Length); Assert.AreEqual(0, fileMarkers.Length);
@@ -78,7 +78,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
// parse sql statement // parse sql statement
var scriptFile = new ScriptFile(); var scriptFile = new ScriptFile();
scriptFile.SetFileContents(sqlWithErrors); scriptFile.SetFileContents(sqlWithErrors);
ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile).GetAwaiter().GetResult();
// verify there is one error // verify there is one error
Assert.AreEqual(1, fileMarkers.Length); Assert.AreEqual(1, fileMarkers.Length);
@@ -97,7 +97,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
public void ParseMultilineSqlWithErrors() public void ParseMultilineSqlWithErrors()
{ {
// multiline sql with errors // multiline sql with errors
const string sqlWithErrors = const string sqlWithErrors =
"SELECT *** FROM sys.objects;\n" + "SELECT *** FROM sys.objects;\n" +
"GO\n" + "GO\n" +
"SELECT *** FROM sys.objects;\n"; "SELECT *** FROM sys.objects;\n";
@@ -108,7 +108,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
// parse sql // parse sql
var scriptFile = new ScriptFile(); var scriptFile = new ScriptFile();
scriptFile.SetFileContents(sqlWithErrors); scriptFile.SetFileContents(sqlWithErrors);
ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile).GetAwaiter().GetResult();
// verify there are two errors // verify there are two errors
Assert.AreEqual(2, fileMarkers.Length); Assert.AreEqual(2, fileMarkers.Length);
@@ -140,29 +140,29 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
scriptFile.SetFileContents(docContent); scriptFile.SetFileContents(docContent);
// When requesting SignatureHelp // When requesting SignatureHelp
SignatureHelp signatureHelp = service.GetSignatureHelp(TestObjects.GetTestDocPosition(), scriptFile); SignatureHelp signatureHelp = service.GetSignatureHelp(TestObjects.GetTestDocPosition(), scriptFile).GetAwaiter().GetResult();
// Then null is returned as no parse info can be used to find the signature // Then null is returned as no parse info can be used to find the signature
Assert.Null(signatureHelp); Assert.Null(signatureHelp);
} }
[Test] [Test]
public void EmptyCompletionListTest() public void EmptyCompletionListTest()
{ {
Assert.AreEqual(0, AutoCompleteHelper.EmptyCompletionList.Length); Assert.AreEqual(0, AutoCompleteHelper.EmptyCompletionList.Length);
} }
internal sealed class TestScriptDocumentInfo : ScriptDocumentInfo internal sealed class TestScriptDocumentInfo : ScriptDocumentInfo
{ {
public TestScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ScriptParseInfo scriptParseInfo, public TestScriptDocumentInfo(TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ScriptParseInfo scriptParseInfo,
string tokenText = null) string tokenText = null)
:base(textDocumentPosition, scriptFile, scriptParseInfo) : base(textDocumentPosition, scriptFile, scriptParseInfo)
{ {
this.tokenText = string.IsNullOrEmpty(tokenText) ? "doesntmatchanythingintheintellisensedefaultlist" : tokenText; this.tokenText = string.IsNullOrEmpty(tokenText) ? "doesntmatchanythingintheintellisensedefaultlist" : tokenText;
} }
private string tokenText; private string tokenText;
public override string TokenText public override string TokenText
{ {
get get
@@ -174,7 +174,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
[Test] [Test]
public void GetDefaultCompletionListWithNoMatchesTest() public void GetDefaultCompletionListWithNoMatchesTest()
{ {
var scriptFile = new ScriptFile(); var scriptFile = new ScriptFile();
scriptFile.SetFileContents("koko wants a bananas"); scriptFile.SetFileContents("koko wants a bananas");
@@ -183,10 +183,10 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
var scriptDocumentInfo = new TestScriptDocumentInfo( var scriptDocumentInfo = new TestScriptDocumentInfo(
new TextDocumentPosition() new TextDocumentPosition()
{ {
TextDocument = new TextDocumentIdentifier() { Uri = TestObjects.ScriptUri }, TextDocument = new TextDocumentIdentifier() { Uri = TestObjects.ScriptUri },
Position = new Position() { Line = 0, Character = 0 } Position = new Position() { Line = 0, Character = 0 }
}, scriptFile, scriptInfo); }, scriptFile, scriptInfo);
AutoCompleteHelper.GetDefaultCompletionItems(scriptDocumentInfo, false); AutoCompleteHelper.GetDefaultCompletionItems(scriptDocumentInfo, false);
} }