/// <summary> /// The core queue processing method /// </summary> /// <param name="state"></param> private void ProcessQueue() { CancellationToken token = this.processQueueCancelToken.Token; WaitHandle[] waitHandles = new WaitHandle[2] { this.itemQueuedEvent, token.WaitHandle }; while (true) { // wait for with an item to be queued or the a cancellation request WaitHandle.WaitAny(waitHandles); if (token.IsCancellationRequested) { break; } try { // dispatch all pending queue items while (this.HasPendingQueueItems) { QueueItem queueItem = GetNextQueueItem(); if (queueItem == null) { continue; } IBindingContext bindingContext = GetOrCreateBindingContext(queueItem.Key); if (bindingContext == null) { queueItem.ItemProcessed.Set(); continue; } bool lockTaken = false; 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)) { queueItem.Result = queueItem.TimeoutOperation != null ? queueItem.TimeoutOperation(bindingContext) : null; continue; } bindingContext.BindingLock.Reset(); lockTaken = true; // execute the binding operation object result = null; CancellationTokenSource cancelToken = new CancellationTokenSource(); var bindTask = Task.Run(() => { result = queueItem.BindOperation( bindingContext, cancelToken.Token); }); // 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); } lockTaken = false; bindTask.ContinueWith((a) => bindingContext.BindingLock.Set()); } } catch (Exception ex) { // catch and log any exceptions raised in the binding calls // set item processed to avoid deadlocks Logger.Write(LogLevel.Error, "Binding queue threw exception " + ex.ToString()); } finally { if (lockTaken) { bindingContext.BindingLock.Set(); } queueItem.ItemProcessed.Set(); } // if a queue processing cancellation was requested then exit the loop if (token.IsCancellationRequested) { break; } } } finally { lock (this.bindingQueueLock) { // verify the binding queue is still empty if (this.bindingQueue.Count == 0) { // reset the item queued event since we've processed all the pending items this.itemQueuedEvent.Reset(); } } } } }
/// <summary> /// Return the completion item list for the current text position. /// This method does not await cache builds since it expects to return quickly /// </summary> /// <param name="textDocumentPosition"></param> public CompletionItem[] GetCompletionItems( TextDocumentPosition textDocumentPosition, ScriptFile scriptFile, ConnectionInfo connInfo) { // initialize some state to parse and bind the current script file this.currentCompletionParseInfo = null; CompletionItem[] resultCompletionItems = null; string filePath = textDocumentPosition.TextDocument.Uri; int startLine = textDocumentPosition.Position.Line; int parserLine = textDocumentPosition.Position.Line + 1; int startColumn = TextUtilities.PositionOfPrevDelimeter( scriptFile.Contents, textDocumentPosition.Position.Line, textDocumentPosition.Position.Character); int endColumn = TextUtilities.PositionOfNextDelimeter( scriptFile.Contents, textDocumentPosition.Position.Line, textDocumentPosition.Position.Character); int parserColumn = textDocumentPosition.Position.Character + 1; bool useLowerCaseSuggestions = this.CurrentSettings.SqlTools.IntelliSense.LowerCaseSuggestions.Value; // get the current script parse info object ScriptParseInfo scriptParseInfo = GetScriptParseInfo(textDocumentPosition.TextDocument.Uri); if (scriptParseInfo == null) { return(AutoCompleteHelper.GetDefaultCompletionItems( startLine, startColumn, endColumn, useLowerCaseSuggestions)); } // reparse and bind the SQL statement if needed if (RequiresReparse(scriptParseInfo, scriptFile)) { ParseAndBind(scriptFile, connInfo); } // if the parse failed then return the default list if (scriptParseInfo.ParseResult == null) { return(AutoCompleteHelper.GetDefaultCompletionItems( startLine, startColumn, endColumn, useLowerCaseSuggestions)); } // need to adjust line & column for base-1 parser indices Token token = GetToken(scriptParseInfo, parserLine, parserColumn); string tokenText = token != null ? token.Text : null; // check if the file is connected and the file lock is available if (scriptParseInfo.IsConnected && Monitor.TryEnter(scriptParseInfo.BuildingMetadataLock)) { try { // queue the completion task with the binding queue QueueItem queueItem = this.BindingQueue.QueueBindingOperation( key: scriptParseInfo.ConnectionKey, bindingTimeout: LanguageService.BindingTimeout, bindOperation: (bindingContext, cancelToken) => { // get the completion list from SQL Parser scriptParseInfo.CurrentSuggestions = Resolver.FindCompletions( scriptParseInfo.ParseResult, parserLine, parserColumn, bindingContext.MetadataDisplayInfoProvider); // cache the current script parse info object to resolve completions later this.currentCompletionParseInfo = scriptParseInfo; // convert the suggestion list to the VS Code format return(AutoCompleteHelper.ConvertDeclarationsToCompletionItems( scriptParseInfo.CurrentSuggestions, startLine, startColumn, endColumn)); }, timeoutOperation: (bindingContext) => { // return the default list if the connected bind fails return(AutoCompleteHelper.GetDefaultCompletionItems( startLine, startColumn, endColumn, useLowerCaseSuggestions, tokenText)); }); // wait for the queue item queueItem.ItemProcessed.WaitOne(); var completionItems = queueItem.GetResultAsT <CompletionItem[]>(); if (completionItems != null && completionItems.Length > 0) { resultCompletionItems = completionItems; } else if (!ShouldShowCompletionList(token)) { resultCompletionItems = AutoCompleteHelper.EmptyCompletionList; } } finally { Monitor.Exit(scriptParseInfo.BuildingMetadataLock); } } // if there are no completions then provide the default list if (resultCompletionItems == null) { resultCompletionItems = AutoCompleteHelper.GetDefaultCompletionItems( startLine, startColumn, endColumn, useLowerCaseSuggestions, tokenText); } return(resultCompletionItems); }
/// <summary> /// Preinitialize the parser and binder with common metadata. /// This should front load the long binding wait to the time the /// connection is established. Once this is completed other binding /// requests should be faster. /// </summary> /// <param name="info"></param> /// <param name="scriptInfo"></param> internal static void PrepopulateCommonMetadata( ConnectionInfo info, ScriptParseInfo scriptInfo, ConnectedBindingQueue bindingQueue) { if (scriptInfo.IsConnected) { var scriptFile = AutoCompleteHelper.WorkspaceServiceInstance.Workspace.GetFile(info.OwnerUri); LanguageService.Instance.ParseAndBind(scriptFile, info); if (Monitor.TryEnter(scriptInfo.BuildingMetadataLock, LanguageService.OnConnectionWaitTimeout)) { try { QueueItem queueItem = bindingQueue.QueueBindingOperation( key: scriptInfo.ConnectionKey, bindingTimeout: AutoCompleteHelper.PrepopulateBindTimeout, waitForLockTimeout: AutoCompleteHelper.PrepopulateBindTimeout, bindOperation: (bindingContext, cancelToken) => { // parse a simple statement that returns common metadata ParseResult parseResult = Parser.Parse( "select ", bindingContext.ParseOptions); 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); // this forces lazy evaluation of the suggestion metadata AutoCompleteHelper.ConvertDeclarationsToCompletionItems(suggestions, 1, 8, 8); parseResult = Parser.Parse( "exec ", bindingContext.ParseOptions); 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 { } finally { Monitor.Exit(scriptInfo.BuildingMetadataLock); } } } }
/// <summary> /// Parses the SQL text and binds it to the SMO metadata provider if connected /// </summary> /// <param name="filePath"></param> /// <param name="sqlText"></param> /// <returns>The ParseResult instance returned from SQL Parser</returns> public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo) { // get or create the current parse info object ScriptParseInfo parseInfo = GetScriptParseInfo(scriptFile.ClientFilePath, createIfNotExists: true); if (Monitor.TryEnter(parseInfo.BuildingMetadataLock, LanguageService.BindingTimeout)) { try { if (connInfo == null || !parseInfo.IsConnected) { // parse current SQL file contents to retrieve a list of errors ParseResult parseResult = Parser.IncrementalParse( scriptFile.Contents, parseInfo.ParseResult, this.DefaultParseOptions); parseInfo.ParseResult = parseResult; } else { QueueItem queueItem = this.BindingQueue.QueueBindingOperation( key: parseInfo.ConnectionKey, bindingTimeout: LanguageService.BindingTimeout, bindOperation: (bindingContext, cancelToken) => { try { ParseResult parseResult = Parser.IncrementalParse( scriptFile.Contents, parseInfo.ParseResult, bindingContext.ParseOptions); parseInfo.ParseResult = parseResult; List <ParseResult> parseResults = new List <ParseResult>(); parseResults.Add(parseResult); bindingContext.Binder.Bind( parseResults, connInfo.ConnectionDetails.DatabaseName, BindMode.Batch); } catch (ConnectionException) { Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object..."); } catch (SqlParserInternalBinderError) { Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object..."); } catch (Exception ex) { Logger.Write(LogLevel.Error, "Unknown exception during parsing " + ex.ToString()); } return(null); }); queueItem.ItemProcessed.WaitOne(); } } catch (Exception ex) { // reset the parse result to do a full parse next time parseInfo.ParseResult = null; Logger.Write(LogLevel.Error, "Unknown exception during parsing " + ex.ToString()); } finally { Monitor.Exit(parseInfo.BuildingMetadataLock); } } else { Logger.Write(LogLevel.Warning, "Binding metadata lock timeout in ParseAndBind"); } return(parseInfo.ParseResult); }
/// <summary> /// The core queue processing method /// </summary> /// <param name="state"></param> private void ProcessQueue() { CancellationToken token = this.processQueueCancelToken.Token; WaitHandle[] waitHandles = new WaitHandle[2] { this.itemQueuedEvent, token.WaitHandle }; while (true) { // wait for with an item to be queued or the a cancellation request WaitHandle.WaitAny(waitHandles); if (token.IsCancellationRequested) { break; } try { // dispatch all pending queue items while (this.HasPendingQueueItems) { QueueItem queueItem = GetNextQueueItem(); if (queueItem == null) { continue; } IBindingContext bindingContext = GetOrCreateBindingContext(queueItem.Key); if (bindingContext == null) { queueItem.ItemProcessed.Set(); continue; } var bindingContextTask = this.BindingContextTasks[bindingContext]; // Run in the binding context task in case this task has to wait for a previous binding operation this.BindingContextTasks[bindingContext] = bindingContextTask.ContinueWith((task) => { bool lockTaken = false; 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.Write(TraceEventType.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.Write(TraceEventType.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.Write(TraceEventType.Error, "Unexpected exception on the binding queue: " + ex.ToString()); if (queueItem.ErrorHandler != null) { try { result = queueItem.ErrorHandler(ex); } catch (Exception ex2) { Logger.Write(TraceEventType.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.Write(TraceEventType.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.Write(TraceEventType.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.Write(TraceEventType.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 (token.IsCancellationRequested) { break; } } } finally { lock (this.bindingQueueLock) { // verify the binding queue is still empty if (this.bindingQueue.Count == 0) { // reset the item queued event since we've processed all the pending items this.itemQueuedEvent.Reset(); } } } } }