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);
}
}