Feature: Progressive Messages (#208)

This change is a reworking of the way that messages are sent to clients from the service layer. It is also a reworking of the protocol to ensure that all formulations of query send back events to the client in a deterministic ordering. To support the first change:
* Added a new event that will be sent when a message is generated
* Messages now indicate which Batch (if any) generated them
* Messages now indicate if they were error level
* Removed message storage in Batch objects and BatchSummary objects
* Batch objects no longer have error state
This commit is contained in:
Benjamin Russell
2017-01-10 16:42:03 -08:00
committed by GitHub
parent a77fb77a85
commit e71bcefb28
16 changed files with 545 additions and 509 deletions

View File

@@ -40,16 +40,16 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary>
private DateTime executionStartTime;
/// <summary>
/// Whether or not any messages have been sent
/// </summary>
private bool messagesSent;
/// <summary>
/// Factory for creating readers/writers for the output of the batch
/// </summary>
private readonly IFileStreamFactory outputFileFactory;
/// <summary>
/// Internal representation of the messages so we can modify internally
/// </summary>
internal readonly List<ResultMessage> resultMessages;
/// <summary>
/// Internal representation of the result sets so we can modify internally
/// </summary>
@@ -71,7 +71,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
HasExecuted = false;
Id = ordinalId;
resultSets = new List<ResultSet>();
resultMessages = new List<ResultMessage>();
this.outputFileFactory = outputFileFactory;
}
@@ -83,11 +82,22 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// <param name="batch">The batch that completed</param>
public delegate Task BatchAsyncEventHandler(Batch batch);
/// <summary>
/// Asynchronous handler for when a message is emitted by the sql connection
/// </summary>
/// <param name="message">The message that was emitted</param>
public delegate Task BatchAsyncMessageHandler(ResultMessage message);
/// <summary>
/// Event that will be called when the batch has completed execution
/// </summary>
public event BatchAsyncEventHandler BatchCompletion;
/// <summary>
/// Event that will be called when a message has been emitted
/// </summary>
public event BatchAsyncMessageHandler BatchMessageSent;
/// <summary>
/// Event to call when the batch has started execution
/// </summary>
@@ -132,11 +142,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary>
public string ExecutionStartTimeStamp { get { return executionStartTime.ToString("o"); } }
/// <summary>
/// Whether or not this batch has an error
/// </summary>
public bool HasError { get; set; }
/// <summary>
/// Whether or not this batch has been executed, regardless of success or failure
/// </summary>
@@ -147,14 +152,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary>
public int Id { get; private set; }
/// <summary>
/// Messages that have come back from the server
/// </summary>
public IEnumerable<ResultMessage> ResultMessages
{
get { return resultMessages; }
}
/// <summary>
/// The result sets of the batch execution
/// </summary>
@@ -187,7 +184,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
// Batch summary with information available at start
BatchSummary summary = new BatchSummary
{
HasError = HasError,
Id = Id,
Selection = Selection,
ExecutionStart = ExecutionStartTimeStamp
@@ -197,7 +193,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
if (HasExecuted)
{
summary.ResultSetSummaries = ResultSummaries;
summary.Messages = ResultMessages.ToArray();
summary.ExecutionEnd = ExecutionEndTimeStamp;
summary.ExecutionElapsed = ExecutionElapsedTime;
}
@@ -244,7 +239,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{
// Register the message listener to *this instance* of the batch
// Note: This is being done to associate messages with batches
sqlConn.GetUnderlyingConnection().InfoMessage += StoreDbMessage;
sqlConn.GetUnderlyingConnection().InfoMessage += ServerMessageHandler;
command = sqlConn.GetUnderlyingConnection().CreateCommand();
// Add a handler for when the command completes
@@ -298,27 +293,25 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
// If there were no messages, for whatever reason (NO COUNT set, messages
// were emitted, records returned), output a "successful" message
if (resultMessages.Count == 0)
if (!messagesSent)
{
resultMessages.Add(new ResultMessage(SR.QueryServiceCompletedSuccessfully));
await SendMessage(SR.QueryServiceCompletedSuccessfully, false);
}
}
}
}
catch (DbException dbe)
{
HasError = true;
UnwrapDbException(dbe);
await UnwrapDbException(dbe);
}
catch (TaskCanceledException)
{
resultMessages.Add(new ResultMessage(SR.QueryServiceQueryCancelled));
await SendMessage(SR.QueryServiceQueryCancelled, false);
throw;
}
catch (Exception e)
{
HasError = true;
resultMessages.Add(new ResultMessage(SR.QueryServiceQueryFailed(e.Message)));
await SendMessage(SR.QueryServiceQueryFailed(e.Message), true);
throw;
}
finally
@@ -327,7 +320,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
ReliableSqlConnection sqlConn = conn as ReliableSqlConnection;
if (sqlConn != null)
{
sqlConn.GetUnderlyingConnection().InfoMessage -= StoreDbMessage;
sqlConn.GetUnderlyingConnection().InfoMessage -= ServerMessageHandler;
}
// Mark that we have executed
@@ -400,6 +393,19 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
#region Private Helpers
private async Task SendMessage(string message, bool isError)
{
// If the message event is null, this is a no-op
if (BatchMessageSent == null)
{
return;
}
// State that we've sent any message, and send it
messagesSent = true;
await BatchMessageSent(new ResultMessage(message, isError, Id));
}
/// <summary>
/// Handler for when the StatementCompleted event is fired for this batch's command. This
/// will be executed ONLY when there is a rowcount to report. If this event is not fired
@@ -410,17 +416,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
internal void StatementCompletedHandler(object sender, StatementCompletedEventArgs args)
{
// Add a message for the number of rows the query returned
string message;
if (args.RecordCount == 1)
{
message = SR.QueryServiceAffectedOneRow;
}
else
{
message = SR.QueryServiceAffectedRows(args.RecordCount);
}
resultMessages.Add(new ResultMessage(message));
string message = args.RecordCount == 1
? SR.QueryServiceAffectedOneRow
: SR.QueryServiceAffectedRows(args.RecordCount);
SendMessage(message, false).Wait();
}
/// <summary>
@@ -430,30 +429,30 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary>
/// <param name="sender">Object that fired the event</param>
/// <param name="args">Arguments from the event</param>
private void StoreDbMessage(object sender, SqlInfoMessageEventArgs args)
private void ServerMessageHandler(object sender, SqlInfoMessageEventArgs args)
{
resultMessages.Add(new ResultMessage(args.Message));
SendMessage(args.Message, false).Wait();
}
/// <summary>
/// Attempts to convert a <see cref="DbException"/> to a <see cref="SqlException"/> that
/// Attempts to convert an <see cref="Exception"/> to a <see cref="SqlException"/> that
/// contains much more info about Sql Server errors. The exception is then unwrapped and
/// messages are formatted and stored in <see cref="ResultMessages"/>. If the exception
/// cannot be converted to SqlException, the message is written to the messages list.
/// messages are formatted and sent to the extension. If the exception cannot be
/// converted to SqlException, the message is written to the messages list.
/// </summary>
/// <param name="dbe">The exception to unwrap</param>
internal void UnwrapDbException(DbException dbe)
private async Task UnwrapDbException(Exception dbe)
{
SqlException se = dbe as SqlException;
if (se != null)
{
var errors = se.Errors.Cast<SqlError>().ToList();
// Detect user cancellation errors
if (errors.Any(error => error.Class == 11 && error.Number == 0))
{
// User cancellation error, add the single message
HasError = false;
resultMessages.Add(new ResultMessage(SR.QueryServiceQueryCancelled));
await SendMessage(SR.QueryServiceQueryCancelled, false);
}
else
{
@@ -464,13 +463,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
string message = string.Format("Msg {0}, Level {1}, State {2}, Line {3}{4}{5}",
error.Number, error.Class, error.State, lineNumber,
Environment.NewLine, error.Message);
resultMessages.Add(new ResultMessage(message));
await SendMessage(message, true);
}
}
}
else
{
resultMessages.Add(new ResultMessage(dbe.Message));
await SendMessage(dbe.Message, true);
}
}

View File

@@ -25,11 +25,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
/// </summary>
public string ExecutionStart { get; set; }
/// <summary>
/// Whether or not the batch was successful. True indicates errors, false indicates success
/// </summary>
public bool HasError { get; set; }
/// <summary>
/// The ID of the result set within the query results
/// </summary>
@@ -40,11 +35,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
/// </summary>
public SelectionData Selection { get; set; }
/// <summary>
/// Any messages that came back from the server during execution of the batch
/// </summary>
public ResultMessage[] Messages { get; set; }
/// <summary>
/// The summaries of the result sets inside the batch
/// </summary>

View File

@@ -21,11 +21,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
/// Summaries of the result sets that were returned with the query
/// </summary>
public BatchSummary[] BatchSummaries { get; set; }
/// <summary>
/// Error message, if any
/// </summary>
public string Message { get; set; }
}
public class QueryExecuteCompleteEvent

View File

@@ -0,0 +1,32 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
{
/// <summary>
/// Parameters to be sent back with a message notification
/// </summary>
public class QueryExecuteMessageParams
{
/// <summary>
/// URI for the editor that owns the query
/// </summary>
public string OwnerUri { get; set; }
/// <summary>
/// The message that is being returned
/// </summary>
public ResultMessage Message { get; set; }
}
public class QueryExecuteMessageEvent
{
public static readonly
EventType<QueryExecuteMessageParams> Type =
EventType<QueryExecuteMessageParams>.Create("query/message");
}
}

View File

@@ -28,10 +28,6 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
/// </summary>
public class QueryExecuteResult
{
/// <summary>
/// Informational messages from the query runner. Optional, can be set to null.
/// </summary>
public string Messages { get; set; }
}
public class QueryExecuteRequest

View File

@@ -12,6 +12,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
/// </summary>
public class ResultMessage
{
/// <summary>
/// ID of the batch that generated this message. If null, this message
/// was not generated as part of a batch
/// </summary>
public int? BatchId { get; set; }
/// <summary>
/// Whether or not this message is an error
/// </summary>
public bool IsError { get; set; }
/// <summary>
/// Timestamp of the message
/// Stored in UTC ISO 8601 format; should be localized before displaying to any user
@@ -23,20 +34,13 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
/// </summary>
public string Message { get; set; }
/// <summary>
/// Full constructor
/// </summary>
public ResultMessage(string timeStamp, string message)
{
Time = timeStamp;
Message = message;
}
/// <summary>
/// Constructor with default "Now" time
/// </summary>
public ResultMessage(string message)
public ResultMessage(string message, bool isError, int? batchId)
{
BatchId = batchId;
IsError = isError;
Time = DateTime.Now.ToString("o");
Message = message;
}

View File

@@ -99,6 +99,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
/// </summary>
public event Batch.BatchAsyncEventHandler BatchCompleted;
/// <summary>
/// Event that will be called when a message has been emitted
/// </summary>
public event Batch.BatchAsyncMessageHandler BatchMessageSent;
/// <summary>
/// Event to be called when a batch starts execution.
/// </summary>
@@ -272,6 +277,14 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
// Don't actually execute if there aren't any batches to execute
if (Batches.Length == 0)
{
if (BatchMessageSent != null)
{
await BatchMessageSent(new ResultMessage(SR.QueryServiceCompletedSuccessfully, false, null));
}
if (QueryCompleted != null)
{
await QueryCompleted(this);
}
return;
}
@@ -308,6 +321,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{
b.BatchStart += BatchStarted;
b.BatchCompletion += BatchCompleted;
b.BatchMessageSent += BatchMessageSent;
b.ResultSetCompletion += ResultSetCompleted;
await b.Execute(conn, cancellationSource.Token);
}

View File

@@ -370,10 +370,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
}
// Send the result stating that the query was successfully started
await requestContext.SendResult(new QueryExecuteResult
{
Messages = newQuery.Batches.Length == 0 ? SR.QueryServiceCompletedSuccessfully : null
});
await requestContext.SendResult(new QueryExecuteResult());
return newQuery;
}
@@ -411,7 +408,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
QueryExecuteCompleteParams eventParams = new QueryExecuteCompleteParams
{
OwnerUri = executeParams.OwnerUri,
Message = errorMessage
//Message = errorMessage
};
await requestContext.SendEvent(QueryExecuteCompleteEvent.Type, eventParams);
};
@@ -443,6 +440,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
};
query.BatchCompleted += batchCompleteCallback;
Batch.BatchAsyncMessageHandler batchMessageCallback = async m =>
{
QueryExecuteMessageParams eventParams = new QueryExecuteMessageParams
{
Message = m,
OwnerUri = executeParams.OwnerUri
};
await requestContext.SendEvent(QueryExecuteMessageEvent.Type, eventParams);
};
query.BatchMessageSent += batchMessageCallback;
// Setup the ResultSet completion callback
ResultSet.ResultSetAsyncEventHandler resultCallback = async r =>
{