Added task service (#374)

* Added task service
This commit is contained in:
Leila Lali
2017-06-12 11:02:57 -07:00
committed by GitHub
parent 04ed01c88d
commit b0263f8867
22 changed files with 1501 additions and 83 deletions

View File

@@ -4,8 +4,10 @@
//
using System;
using System.Threading.Tasks;
using Microsoft.SqlTools.Extensibility;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.Hosting
{
@@ -65,6 +67,23 @@ namespace Microsoft.SqlTools.Hosting
}
}
protected async Task<T> HandleRequestAsync<T>(Func<Task<T>> handler, RequestContext<T> requestContext, string requestType)
{
Logger.Write(LogLevel.Verbose, requestType);
try
{
T result = await handler();
await requestContext.SendResult(result);
return result;
}
catch (Exception ex)
{
await requestContext.SendError(ex.ToString());
}
return default(T);
}
public abstract void InitializeService(IProtocolEndpoint serviceHost);
}

View File

@@ -480,23 +480,6 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
return new ExpandResponse() { SessionId = session.Uri, NodePath = expandParams.NodePath };
}
private async Task<T> HandleRequestAsync<T>(Func<Task<T>> handler, RequestContext<T> requestContext, string requestType)
{
Logger.Write(LogLevel.Verbose, requestType);
try
{
T result = await handler();
await requestContext.SendResult(result);
return result;
}
catch (Exception ex)
{
await requestContext.SendError(ex.ToString());
}
return default(T);
}
/// <summary>
/// Generates a URI for object explorer using a similar pattern to Mongo DB (which has URI-based database definition)
/// as this should ensure uniqueness

View File

@@ -4,22 +4,18 @@
//
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.SqlTools.ServiceLayer.TaskServices.Contracts
{
public class ListTasksParams
{
bool ListActiveTasksOnly { get; set; }
bool ListActiveTasksOnly { get; set; }
}
public class ListTasksResponse
{
TaskInfo[] Tasks { get; set; }
public TaskInfo[] Tasks { get; set; }
}
public class ListTasksRequest

View File

@@ -0,0 +1,24 @@
//
// 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.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.TaskServices.Contracts
{
public class CancelTaskParams
{
/// <summary>
/// An id to unify the task
/// </summary>
public string TaskId { get; set; }
}
public class CancelTaskRequest
{
public static readonly
RequestType<CancelTaskParams, bool> Type =
RequestType<CancelTaskParams, bool>.Create("tasks/canceltask");
}
}

View File

@@ -1,21 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.TaskServices.Contracts
{
public enum TaskState
{
NotStarted = 0,
Running = 1,
Complete = 2
}
public class TaskInfo
{
public int TaskId { get; set; }
/// <summary>
/// An id to unify the task
/// </summary>
public string TaskId { get; set; }
/// <summary>
/// Task status
/// </summary>
public SqlTaskStatus Status { get; set; }
/// <summary>
/// Database server name this task is created for
/// </summary>
public string ServerName { get; set; }
/// <summary>
/// Database name this task is created for
/// </summary>
public string DatabaseName { get; set; }
/// <summary>
/// Task name which defines the type of the task (e.g. CreateDatabase, Backup)
/// </summary>
public string Name { get; set; }
/// <summary>
/// Task description
/// </summary>
public string Description { get; set; }
public TaskState State { get; set; }
}
}

View File

@@ -0,0 +1,29 @@
//
// 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.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.TaskServices.Contracts
{
/// <summary>
/// Expand notification mapping entry
/// </summary>
public class TaskCreatedNotification
{
public static readonly
EventType<TaskInfo> Type =
EventType<TaskInfo>.Create("task/newtaskcreated");
}
/// <summary>
/// Expand notification mapping entry
/// </summary>
public class TaskStatusChangedNotification
{
public static readonly
EventType<TaskProgressInfo> Type =
EventType<TaskProgressInfo>.Create("task/statuschanged");
}
}

View File

@@ -0,0 +1,31 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.TaskServices.Contracts
{
public class TaskProgressInfo
{
/// <summary>
/// An id to unify the task
/// </summary>
public string TaskId { get; set; }
/// <summary>
/// Task status
/// </summary>
public SqlTaskStatus Status { get; set; }
/// <summary>
/// Database server name this task is created for
/// </summary>
public string Message { get; set; }
/// <summary>
/// The number of millisecond the task was running
/// </summary>
public double Duration { get; set; }
}
}

View File

@@ -0,0 +1,413 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.TaskServices.Contracts;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.TaskServices
{
/// <summary>
/// A wrapper to a long running database operation. The class holds a refrence to the actual task that's running
/// and keeps track of the task status to send notifications
/// </summary>
public class SqlTask : IDisposable
{
private bool isCompleted;
private bool isCanceled;
private bool isDisposed;
private readonly object lockObject = new object();
private readonly List<TaskMessage> messages = new List<TaskMessage>();
private DateTime startTime;
private SqlTaskStatus status = SqlTaskStatus.NotStarted;
private DateTime stopTime;
public event EventHandler<TaskEventArgs<TaskMessage>> MessageAdded;
public event EventHandler<TaskEventArgs<SqlTaskStatus>> StatusChanged;
public event EventHandler<TaskEventArgs<SqlTaskStatus>> TaskCanceled;
/// <summary>
/// Creates new instance of SQL task
/// </summary>
/// <param name="taskMetdata">Task Metadata</param>
/// <param name="testToRun">The function to run to start the task</param>
public SqlTask(TaskMetadata taskMetdata, Func<SqlTask, Task<TaskResult>> testToRun)
{
Validate.IsNotNull(nameof(taskMetdata), taskMetdata);
Validate.IsNotNull(nameof(testToRun), testToRun);
TaskMetadata = taskMetdata;
TaskToRun = testToRun;
StartTime = DateTime.UtcNow;
TaskId = Guid.NewGuid();
}
/// <summary>
/// Task Metadata
/// </summary>
internal TaskMetadata TaskMetadata { get; private set; }
/// <summary>
/// The function to run
/// </summary>
private Func<SqlTask, Task<TaskResult>> TaskToRun
{
get;
set;
}
/// <summary>
/// Task unique id
/// </summary>
public Guid TaskId { get; private set; }
/// <summary>
/// Starts the task and monitor the task progress
/// </summary>
public async Task Run()
{
TaskStatus = SqlTaskStatus.InProgress;
await TaskToRun(this).ContinueWith(task =>
{
if (task.IsCompleted)
{
TaskResult taskResult = task.Result;
TaskStatus = taskResult.TaskStatus;
}
else if(task.IsCanceled)
{
TaskStatus = SqlTaskStatus.Canceled;
}
else if(task.IsFaulted)
{
TaskStatus = SqlTaskStatus.Failed;
if(task.Exception != null)
{
AddMessage(task.Exception.Message);
}
}
});
}
/// <summary>
/// Returns true if task has any messages
/// </summary>
public bool HasMessages
{
get
{
lock (lockObject)
{
return messages.Any();
}
}
}
/// <summary>
/// Setting this to True will not change the Slot status.
/// Setting the Slot status to Canceled will set this to true.
/// </summary>
public bool IsCanceled
{
get
{
return isCanceled;
}
private set
{
if (isCanceled != value)
{
isCanceled = value;
OnTaskCanceled();
}
}
}
/// <summary>
/// Returns true if task is canceled, failed or succeed
/// </summary>
public bool IsCompleted
{
get
{
return isCompleted;
}
private set
{
if (isCompleted != value)
{
isCompleted = value;
if (isCompleted)
{
StopTime = DateTime.UtcNow;
}
}
}
}
/// <summary>
/// Task Messages
/// </summary>
internal ReadOnlyCollection<TaskMessage> Messages
{
get
{
lock (lockObject)
{
return messages.AsReadOnly();
}
}
}
/// <summary>
/// Start Time
/// </summary>
public DateTime StartTime
{
get
{
return startTime;
}
internal set
{
startTime = value;
}
}
/// <summary>
/// The total number of seconds to run the task
/// </summary>
public double Duration
{
get
{
return (stopTime - startTime).TotalMilliseconds;
}
}
/// <summary>
/// Task Status
/// </summary>
public SqlTaskStatus TaskStatus
{
get
{
return status;
}
private set
{
status = value;
switch (status)
{
case SqlTaskStatus.Canceled:
case SqlTaskStatus.Failed:
case SqlTaskStatus.Succeeded:
case SqlTaskStatus.SucceededWithWarning:
IsCompleted = true;
break;
case SqlTaskStatus.InProgress:
case SqlTaskStatus.NotStarted:
IsCompleted = false;
break;
default:
throw new NotSupportedException("IsCompleted is not determined for status: " + status);
}
if (status == SqlTaskStatus.Canceled)
{
IsCanceled = true;
}
OnStatusChanged();
}
}
/// <summary>
/// The date time that the task was complete
/// </summary>
public DateTime StopTime
{
get
{
return stopTime;
}
internal set
{
stopTime = value;
}
}
/// <summary>
/// Try to cancel the task, and even to cancel the task will be raised
/// but the status won't change until that task actually get canceled by it's owner
/// </summary>
public void Cancel()
{
IsCanceled = true;
}
/// <summary>
/// Adds a new message to the task messages
/// </summary>
/// <param name="description">Message description</param>
/// <param name="status">Status of the message</param>
/// <param name="insertAboveLast">If true, the new messages will be added to the top. Default is false</param>
/// <returns></returns>
public TaskMessage AddMessage(string description, SqlTaskStatus status = SqlTaskStatus.NotStarted, bool insertAboveLast = false)
{
ValidateNotDisposed();
if (!insertAboveLast)
{
// Make sure the last message is set to a completed status if a new message is being added at the bottom
CompleteLastMessageStatus();
}
var newMessage = new TaskMessage
{
Description = description,
Status = status,
};
lock (lockObject)
{
if (!insertAboveLast || messages.Count == 0)
{
messages.Add(newMessage);
}
else
{
int lastMessageIndex = messages.Count - 1;
messages.Insert(lastMessageIndex, newMessage);
}
}
OnMessageAdded(new TaskEventArgs<TaskMessage>(newMessage, this));
// If the slot is completed, this may be the last message, make sure the message is also set to completed.
if (IsCompleted)
{
CompleteLastMessageStatus();
}
return newMessage;
}
/// <summary>
/// Converts the task to Task info to be used in the contracts
/// </summary>
/// <returns></returns>
public TaskInfo ToTaskInfo()
{
return new TaskInfo
{
DatabaseName = TaskMetadata.DatabaseName,
ServerName = TaskMetadata.ServerName,
Name = TaskMetadata.Name,
Description = TaskMetadata.Description,
TaskId = TaskId.ToString()
};
}
/// <summary>
/// Makes sure the last message has a 'completed' status if it has a status of InProgress.
/// If success is true, then sets the status to Succeeded. Sets it to Failed if success is false.
/// If success is null (default), then the message status is based on the status of the slot.
/// </summary>
private void CompleteLastMessageStatus(bool? success = null)
{
var message = GetLastMessage();
if (message != null)
{
if (message.Status == SqlTaskStatus.InProgress)
{
// infer the success boolean from the slot status if it's not set
if (success == null)
{
switch (TaskStatus)
{
case SqlTaskStatus.Canceled:
case SqlTaskStatus.Failed:
success = false;
break;
default:
success = true;
break;
}
}
message.Status = success.Value ? SqlTaskStatus.Succeeded : SqlTaskStatus.Failed;
}
}
}
private void OnMessageAdded(TaskEventArgs<TaskMessage> e)
{
var handler = MessageAdded;
if (handler != null)
{
handler(this, e);
}
}
private void OnStatusChanged()
{
var handler = StatusChanged;
if (handler != null)
{
handler(this, new TaskEventArgs<SqlTaskStatus>(TaskStatus, this));
}
}
private void OnTaskCanceled()
{
var handler = TaskCanceled;
if (handler != null)
{
handler(this, new TaskEventArgs<SqlTaskStatus>(TaskStatus, this));
}
}
public void Dispose()
{
//Dispose
isDisposed = true;
}
protected void ValidateNotDisposed()
{
if (isDisposed)
{
throw new ObjectDisposedException(typeof(SqlTask).FullName);
}
}
/// <summary>
/// Returns the most recently created message. Returns null if there are no messages on the slot.
/// </summary>
public TaskMessage GetLastMessage()
{
ValidateNotDisposed();
lock (lockObject)
{
if (messages.Count > 0)
{
// get
return messages.Last();
}
}
return null;
}
}
}

View File

@@ -0,0 +1,210 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.SqlTools.ServiceLayer.TaskServices
{
/// <summary>
/// A singleton class to manager the current long running operations
/// </summary>
public class SqlTaskManager : IDisposable
{
private static SqlTaskManager instance = new SqlTaskManager();
private static readonly object lockObject = new object();
private bool isDisposed;
private readonly ConcurrentDictionary<Guid, SqlTask> tasks = new ConcurrentDictionary<Guid, SqlTask>();
public event EventHandler<TaskEventArgs<SqlTask>> TaskAdded;
public event EventHandler<TaskEventArgs<SqlTask>> TaskRemoved;
/// <summary>
/// Constructor to create an instance for test purposes use only
/// </summary>
internal SqlTaskManager()
{
}
/// <summary>
/// Singleton instance
/// </summary>
public static SqlTaskManager Instance
{
get
{
return instance;
}
}
/// <summary>
/// Task connections
/// </summary>
internal ReadOnlyCollection<SqlTask> Tasks
{
get
{
lock (lockObject)
{
return new ReadOnlyCollection<SqlTask>(tasks.Values.ToList());
}
}
}
/// <summary>
/// Clear completed tasks
/// </summary>
internal void ClearCompletedTasks()
{
ValidateNotDisposed();
lock (lockObject)
{
var tasksToRemove = (from task in tasks.Values
where task.IsCompleted
select task).ToList();
foreach (var task in tasksToRemove)
{
RemoveCompletedTask(task);
}
}
}
/// <summary>
/// Creates a new task
/// </summary>
/// <param name="taskMetadata">Task Metadata</param>
/// <param name="taskToRun">The function to run the operation</param>
/// <returns></returns>
public SqlTask CreateTask(TaskMetadata taskMetadata, Func<SqlTask, Task<TaskResult>> taskToRun)
{
ValidateNotDisposed();
var newtask = new SqlTask(taskMetadata, taskToRun );
lock (lockObject)
{
tasks.AddOrUpdate(newtask.TaskId, newtask, (key, oldValue) => newtask);
}
OnTaskAdded(new TaskEventArgs<SqlTask>(newtask));
return newtask;
}
public void Dispose()
{
Dispose(true);
}
void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
if (disposing)
{
lock (lockObject)
{
foreach (var task in tasks.Values)
{
task.Dispose();
}
tasks.Clear();
}
}
isDisposed = true;
}
/// <summary>
/// Returns true if there's any completed task
/// </summary>
/// <returns></returns>
internal bool HasCompletedTasks()
{
lock (lockObject)
{
return tasks.Values.Any(task => task.IsCompleted);
}
}
private void OnTaskAdded(TaskEventArgs<SqlTask> e)
{
var handler = TaskAdded;
if (handler != null)
{
handler(this, e);
}
}
private void OnTaskRemoved(TaskEventArgs<SqlTask> e)
{
var handler = TaskRemoved;
if (handler != null)
{
handler(this, e);
}
}
/// <summary>
/// Cancel a task
/// </summary>
/// <param name="taskId"></param>
public void CancelTask(Guid taskId)
{
SqlTask taskToCancel;
lock (lockObject)
{
tasks.TryGetValue(taskId, out taskToCancel);
}
if(taskToCancel != null)
{
taskToCancel.Cancel();
}
}
/// <summary>
/// Internal for test purposes only.
/// Removes all tasks regardless of status from the model.
/// This is used as a test aid since Monitor is a singleton class.
/// </summary>
internal void Reset()
{
foreach (var task in tasks.Values)
{
RemoveTask(task);
}
}
internal void RemoveCompletedTask(SqlTask task)
{
if (task.IsCompleted)
{
RemoveTask(task);
}
}
private void RemoveTask(SqlTask task)
{
SqlTask removedTask;
tasks.TryRemove(task.TaskId, out removedTask);
}
void ValidateNotDisposed()
{
if (isDisposed)
{
throw new ObjectDisposedException(typeof(SqlTaskManager).FullName);
}
}
}
}

View File

@@ -0,0 +1,40 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.TaskServices
{
public sealed class TaskEventArgs<T> : EventArgs
{
readonly T taskData;
public TaskEventArgs(T taskData, SqlTask sqlTask)
{
Validate.IsNotNull(nameof(taskData), taskData);
this.taskData = taskData;
SqlTask = sqlTask;
}
public TaskEventArgs(SqlTask sqlTask)
{
taskData = (T)Convert.ChangeType(sqlTask, typeof(T));
SqlTask = sqlTask;
}
public T TaskData
{
get
{
return taskData;
}
}
public SqlTask SqlTask { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.TaskServices
{
public class TaskMessage
{
public SqlTaskStatus Status { get; set; }
public string Description { get; set; }
}
}

View File

@@ -0,0 +1,41 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.TaskServices
{
public class TaskMetadata
{
/// <summary>
/// Task Description
/// </summary>
public string Description { get; set; }
/// <summary>
/// Task name to define the type of the task e.g. Create Db, back up
/// </summary>
public string Name { get; set; }
/// <summary>
/// The number of seconds to wait before canceling the task.
/// This is a optional field and 0 or negative numbers means no timeout
/// </summary>
public int Timeout { get; set; }
/// <summary>
/// Defines if the task can be canceled
/// </summary>
public bool IsCancelable { get; set; }
/// <summary>
/// Database server name this task is created for
/// </summary>
public string ServerName { get; set; }
/// <summary>
/// Database name this task is created for
/// </summary>
public string DatabaseName { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.TaskServices
{
public class TaskResult
{
public SqlTaskStatus TaskStatus { get; set; }
public string ErrorMessage { get; set; }
}
}

View File

@@ -4,23 +4,29 @@
//
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.TaskServices.Contracts;
using System;
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting;
using Microsoft.SqlTools.Extensibility;
using Microsoft.SqlTools.Utility;
using System.Linq;
namespace Microsoft.SqlTools.ServiceLayer.TaskServices
{
public class TaskService
public class TaskService: HostedService<TaskService>, IComposableService
{
private static readonly Lazy<TaskService> instance = new Lazy<TaskService>(() => new TaskService());
private SqlTaskManager taskManager = SqlTaskManager.Instance;
private IProtocolEndpoint serviceHost;
/// <summary>
/// Default, parameterless constructor.
/// </summary>
internal TaskService()
public TaskService()
{
taskManager.TaskAdded += OnTaskAdded;
}
/// <summary>
@@ -31,22 +37,128 @@ namespace Microsoft.SqlTools.ServiceLayer.TaskServices
get { return instance.Value; }
}
/// <summary>
/// Task Manager Instance to use for testing
/// </summary>
internal SqlTaskManager TaskManager
{
get
{
return taskManager;
}
}
/// <summary>
/// Initializes the service instance
/// </summary>
public void InitializeService(ServiceHost serviceHost, SqlToolsContext context)
public override void InitializeService(IProtocolEndpoint serviceHost)
{
this.serviceHost = serviceHost;
Logger.Write(LogLevel.Verbose, "TaskService initialized");
serviceHost.SetRequestHandler(ListTasksRequest.Type, HandleListTasksRequest);
serviceHost.SetRequestHandler(CancelTaskRequest.Type, HandleCancelTaskRequest);
}
/// <summary>
/// Handles a list tasks request
/// </summary>
internal static async Task HandleListTasksRequest(
internal async Task HandleListTasksRequest(
ListTasksParams listTasksParams,
RequestContext<ListTasksResponse> requestContext)
RequestContext<ListTasksResponse> context)
{
await requestContext.SendResult(new ListTasksResponse());
Logger.Write(LogLevel.Verbose, "HandleListTasksRequest");
Func<Task<ListTasksResponse>> getAllTasks = () =>
{
Validate.IsNotNull(nameof(listTasksParams), listTasksParams);
return Task.Factory.StartNew(() =>
{
ListTasksResponse response = new ListTasksResponse();
response.Tasks = taskManager.Tasks.Select(x => x.ToTaskInfo()).ToArray();
return response;
});
};
await HandleRequestAsync(getAllTasks, context, "HandleListTasksRequest");
}
internal async Task HandleCancelTaskRequest(CancelTaskParams cancelTaskParams, RequestContext<bool> context)
{
Logger.Write(LogLevel.Verbose, "HandleCancelTaskRequest");
Func<Task<bool>> cancelTask = () =>
{
Validate.IsNotNull(nameof(cancelTaskParams), cancelTaskParams);
return Task.Factory.StartNew(() =>
{
Guid taskId;
if (Guid.TryParse(cancelTaskParams.TaskId, out taskId))
{
taskManager.CancelTask(taskId);
return true;
}
else
{
return false;
}
});
};
await HandleRequestAsync(cancelTask, context, "HandleCancelTaskRequest");
}
private async void OnTaskAdded(object sender, TaskEventArgs<SqlTask> e)
{
SqlTask sqlTask = e.TaskData;
if (sqlTask != null)
{
TaskInfo taskInfo = sqlTask.ToTaskInfo();
sqlTask.MessageAdded += OnTaskMessageAdded;
sqlTask.StatusChanged += OnTaskStatusChanged;
await serviceHost.SendEvent(TaskCreatedNotification.Type, taskInfo);
}
}
private async void OnTaskStatusChanged(object sender, TaskEventArgs<SqlTaskStatus> e)
{
SqlTask sqlTask = e.SqlTask;
if (sqlTask != null)
{
TaskProgressInfo progressInfo = new TaskProgressInfo
{
TaskId = sqlTask.TaskId.ToString(),
Status = e.TaskData
};
if (sqlTask.IsCompleted)
{
progressInfo.Duration = sqlTask.Duration;
}
await serviceHost.SendEvent(TaskStatusChangedNotification.Type, progressInfo);
}
}
private async void OnTaskMessageAdded(object sender, TaskEventArgs<TaskMessage> e)
{
SqlTask sqlTask = e.SqlTask;
if (sqlTask != null)
{
TaskProgressInfo progressInfo = new TaskProgressInfo
{
TaskId = sqlTask.TaskId.ToString(),
Message = e.TaskData.Description
};
await serviceHost.SendEvent(TaskStatusChangedNotification.Type, progressInfo);
}
}
public void Dispose()
{
taskManager.TaskAdded -= OnTaskAdded;
taskManager.Dispose();
}
}
}

View File

@@ -0,0 +1,18 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.TaskServices
{
public enum SqlTaskStatus
{
NotStarted,
InProgress,
Succeeded,
SucceededWithWarning,
Failed,
Canceled
}
}

View File

@@ -396,30 +396,5 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer
ServerInfo = TestObjects.GetTestServerInfo()
};
}
private async Task RunAndVerify<T, TResult>(Func<RequestContext<T>, Task<TResult>> test, Action<TResult> verify)
{
T result = default(T);
var contextMock = RequestContextMocks.Create<T>(r => result = r).AddErrorHandling(null);
TResult actualResult = await test(contextMock.Object);
if (actualResult == null && typeof(TResult) == typeof(T))
{
actualResult = (TResult)Convert.ChangeType(result, typeof(TResult));
}
VerifyResult(contextMock, verify, actualResult);
}
private void VerifyResult<T, TResult>(Mock<RequestContext<T>> contextMock, Action<TResult> verify, TResult actual)
{
contextMock.Verify(c => c.SendResult(It.IsAny<T>()), Times.Once);
contextMock.Verify(c => c.SendError(It.IsAny<string>(), It.IsAny<int>()), Times.Never);
verify(actual);
}
private void VerifyErrorSent<T>(Mock<RequestContext<T>> contextMock)
{
contextMock.Verify(c => c.SendResult(It.IsAny<T>()), Times.Never);
contextMock.Verify(c => c.SendError(It.IsAny<string>(), It.IsAny<int>()), Times.Once);
}
}
}

View File

@@ -12,27 +12,16 @@ using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer
{
// Base class providing common test functionality for OE tests
public abstract class ObjectExplorerTestBase
public abstract class ObjectExplorerTestBase : ServiceTestBase
{
protected RegisteredServiceProvider ServiceProvider
{
get;
set;
}
protected RegisteredServiceProvider CreateServiceProviderWithMinServices()
protected override RegisteredServiceProvider CreateServiceProviderWithMinServices()
{
return CreateProvider()
.RegisterSingleService(new ConnectionService())
.RegisterSingleService(new ObjectExplorerService());
}
protected RegisteredServiceProvider CreateProvider()
{
ServiceProvider = new RegisteredServiceProvider();
return ServiceProvider;
}
protected ObjectExplorerService CreateOEService(ConnectionService connService)
{
CreateProvider()

View File

@@ -0,0 +1,71 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Threading.Tasks;
using Microsoft.SqlTools.Extensibility;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility;
using Moq;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests
{
public abstract class ServiceTestBase
{
protected RegisteredServiceProvider ServiceProvider
{
get;
set;
}
protected RegisteredServiceProvider CreateProvider()
{
ServiceProvider = new RegisteredServiceProvider();
return ServiceProvider;
}
protected abstract RegisteredServiceProvider CreateServiceProviderWithMinServices();
protected async Task RunAndVerify<T, TResult>(Func<RequestContext<T>, Task<TResult>> test, Action<TResult> verify)
{
T result = default(T);
var contextMock = RequestContextMocks.Create<T>(r => result = r).AddErrorHandling(null);
TResult actualResult = await test(contextMock.Object);
if (actualResult == null && typeof(TResult) == typeof(T))
{
actualResult = (TResult)Convert.ChangeType(result, typeof(TResult));
}
VerifyResult<T, TResult>(contextMock, verify, actualResult);
}
protected async Task RunAndVerify<T>(Func<RequestContext<T>, Task> test, Action<T> verify)
{
T result = default(T);
var contextMock = RequestContextMocks.Create<T>(r => result = r).AddErrorHandling(null);
await test(contextMock.Object);
VerifyResult<T>(contextMock, verify, result);
}
protected void VerifyResult<T, TResult>(Mock<RequestContext<T>> contextMock, Action<TResult> verify, TResult actual)
{
contextMock.Verify(c => c.SendResult(It.IsAny<T>()), Times.Once);
contextMock.Verify(c => c.SendError(It.IsAny<string>(), It.IsAny<int>()), Times.Never);
verify(actual);
}
protected void VerifyResult<T>(Mock<RequestContext<T>> contextMock, Action<T> verify, T actual)
{
contextMock.Verify(c => c.SendResult(It.IsAny<T>()), Times.Once);
contextMock.Verify(c => c.SendError(It.IsAny<string>(), It.IsAny<int>()), Times.Never);
verify(actual);
}
protected void VerifyErrorSent<T>(Mock<RequestContext<T>> contextMock)
{
contextMock.Verify(c => c.SendResult(It.IsAny<T>()), Times.Never);
contextMock.Verify(c => c.SendError(It.IsAny<string>(), It.IsAny<int>()), Times.Once);
}
}
}

View File

@@ -0,0 +1,61 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.TaskServices;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
{
public class DatabaseOperationStub
{
private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
public void Stop()
{
IsStopped = true;
}
public void FailTheOperation()
{
Failed = true;
}
public TaskResult TaskResult { get; set; }
public bool IsStopped { get; set; }
public bool Failed { get; set; }
public async Task<TaskResult> FunctionToRun(SqlTask sqlTask)
{
sqlTask.TaskCanceled += OnTaskCanceled;
return await Task.Factory.StartNew(() =>
{
while (!IsStopped)
{
//Just keep running
if (cancellationTokenSource.Token.IsCancellationRequested)
{
throw new OperationCanceledException();
}
if (Failed)
{
throw new InvalidOperationException();
}
sqlTask.AddMessage("still running", SqlTaskStatus.InProgress, true);
}
sqlTask.AddMessage("done!", SqlTaskStatus.Succeeded);
return TaskResult;
});
}
private void OnTaskCanceled(object sender, TaskEventArgs<SqlTaskStatus> e)
{
cancellationTokenSource.Cancel();
}
}
}

View File

@@ -0,0 +1,139 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using Microsoft.SqlTools.ServiceLayer.TaskServices;
using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
{
public class SqlTaskTests
{
[Fact]
public void CreateSqlTaskGivenInvalidArgumentShouldThrowException()
{
Assert.Throws<ArgumentNullException>(() => new SqlTask(null, new DatabaseOperationStub().FunctionToRun));
Assert.Throws<ArgumentNullException>(() => new SqlTask(new TaskMetadata(), null));
}
[Fact]
public void CreateSqlTaskShouldGenerateANewId()
{
SqlTask sqlTask = new SqlTask(new TaskMetadata(), new DatabaseOperationStub().FunctionToRun);
Assert.NotNull(sqlTask.TaskId);
Assert.True(sqlTask.TaskId != Guid.Empty);
SqlTask sqlTask2 = new SqlTask(new TaskMetadata(), new DatabaseOperationStub().FunctionToRun);
Assert.False(sqlTask.TaskId.CompareTo(sqlTask2.TaskId) == 0);
}
[Fact]
public void RunShouldRunTheFunctionAndGetTheResult()
{
SqlTaskStatus expectedStatus = SqlTaskStatus.Succeeded;
DatabaseOperationStub operation = new DatabaseOperationStub();
operation.TaskResult = new TaskResult
{
TaskStatus = expectedStatus
};
SqlTask sqlTask = new SqlTask(new TaskMetadata(), operation.FunctionToRun);
Assert.Equal(sqlTask.TaskStatus, SqlTaskStatus.NotStarted);
sqlTask.Run().ContinueWith(task => {
Assert.Equal(sqlTask.TaskStatus, expectedStatus);
Assert.Equal(sqlTask.IsCompleted, true);
Assert.True(sqlTask.Duration > 0);
});
Assert.Equal(sqlTask.TaskStatus, SqlTaskStatus.InProgress);
operation.Stop();
}
[Fact]
public void ToTaskInfoShouldReturnTaskInfo()
{
SqlTaskStatus expectedStatus = SqlTaskStatus.Succeeded;
DatabaseOperationStub operation = new DatabaseOperationStub();
operation.TaskResult = new TaskResult
{
TaskStatus = expectedStatus
};
SqlTask sqlTask = new SqlTask(new TaskMetadata
{
ServerName = "server name",
DatabaseName = "database name"
}, operation.FunctionToRun);
sqlTask.Run().ContinueWith(task =>
{
var taskInfo = sqlTask.ToTaskInfo();
Assert.Equal(taskInfo.TaskId, sqlTask.TaskId.ToString());
Assert.Equal(taskInfo.ServerName, "server name");
Assert.Equal(taskInfo.DatabaseName, "database name");
});
operation.Stop();
}
[Fact]
public void FailedOperationShouldReturnTheFailedResult()
{
SqlTaskStatus expectedStatus = SqlTaskStatus.Failed;
DatabaseOperationStub operation = new DatabaseOperationStub();
operation.TaskResult = new TaskResult
{
TaskStatus = expectedStatus
};
SqlTask sqlTask = new SqlTask(new TaskMetadata(), operation.FunctionToRun);
Assert.Equal(sqlTask.TaskStatus, SqlTaskStatus.NotStarted);
sqlTask.Run().ContinueWith(task => {
Assert.Equal(sqlTask.TaskStatus, expectedStatus);
Assert.Equal(sqlTask.IsCompleted, true);
Assert.True(sqlTask.Duration > 0);
});
Assert.Equal(sqlTask.TaskStatus, SqlTaskStatus.InProgress);
operation.Stop();
}
[Fact]
public void CancelingTheTaskShouldCancelTheOperation()
{
SqlTaskStatus expectedStatus = SqlTaskStatus.Canceled;
DatabaseOperationStub operation = new DatabaseOperationStub();
operation.TaskResult = new TaskResult
{
};
SqlTask sqlTask = new SqlTask(new TaskMetadata(), operation.FunctionToRun);
Assert.Equal(sqlTask.TaskStatus, SqlTaskStatus.NotStarted);
sqlTask.Run().ContinueWith(task => {
Assert.Equal(sqlTask.TaskStatus, expectedStatus);
Assert.Equal(sqlTask.IsCanceled, true);
Assert.True(sqlTask.Duration > 0);
});
Assert.Equal(sqlTask.TaskStatus, SqlTaskStatus.InProgress);
sqlTask.Cancel();
}
[Fact]
public void FailedOperationShouldFailTheTask()
{
SqlTaskStatus expectedStatus = SqlTaskStatus.Failed;
DatabaseOperationStub operation = new DatabaseOperationStub();
operation.TaskResult = new TaskResult
{
};
SqlTask sqlTask = new SqlTask(new TaskMetadata(), operation.FunctionToRun);
Assert.Equal(sqlTask.TaskStatus, SqlTaskStatus.NotStarted);
sqlTask.Run().ContinueWith(task => {
Assert.Equal(sqlTask.TaskStatus, expectedStatus);
Assert.Equal(sqlTask.IsCanceled, true);
Assert.True(sqlTask.Duration > 0);
});
Assert.Equal(sqlTask.TaskStatus, SqlTaskStatus.InProgress);
operation.FailTheOperation();
}
}
}

View File

@@ -0,0 +1,86 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using Microsoft.SqlTools.ServiceLayer.TaskServices;
using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
{
public class TaskManagerTests
{
private TaskMetadata taskMetaData = new TaskMetadata
{
ServerName = "server name",
DatabaseName = "database name"
};
[Fact]
public void ManagerInstanceWithNoTaskShouldNotBreakOnCancelTask()
{
SqlTaskManager manager = new SqlTaskManager();
Assert.True(manager.Tasks.Count == 0);
manager.CancelTask(Guid.NewGuid());
}
[Fact]
public void VerifyCreateAndRunningTask()
{
using (SqlTaskManager manager = new SqlTaskManager())
{
bool taskAddedEventRaised = false;
manager.TaskAdded += (object sender, TaskEventArgs<SqlTask> e) =>
{
taskAddedEventRaised = true;
};
DatabaseOperationStub operation = new DatabaseOperationStub();
operation.TaskResult = new TaskResult
{
};
SqlTask sqlTask = manager.CreateTask(taskMetaData, operation.FunctionToRun);
Assert.NotNull(sqlTask);
Assert.True(taskAddedEventRaised);
Assert.False(manager.HasCompletedTasks());
sqlTask.Run().ContinueWith(task =>
{
Assert.True(manager.HasCompletedTasks());
manager.RemoveCompletedTask(sqlTask);
});
operation.Stop();
}
}
[Fact]
public void CancelTaskShouldCancelTheOperation()
{
using (SqlTaskManager manager = new SqlTaskManager())
{
SqlTaskStatus expectedStatus = SqlTaskStatus.Canceled;
DatabaseOperationStub operation = new DatabaseOperationStub();
operation.TaskResult = new TaskResult
{
};
SqlTask sqlTask = manager.CreateTask(taskMetaData, operation.FunctionToRun);
Assert.NotNull(sqlTask);
sqlTask.Run().ContinueWith(task =>
{
Assert.Equal(sqlTask.TaskStatus, expectedStatus);
Assert.Equal(sqlTask.IsCanceled, true);
manager.Reset();
});
manager.CancelTask(sqlTask.TaskId);
}
}
}
}

View File

@@ -0,0 +1,131 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SqlTools.Extensibility;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.TaskServices;
using Microsoft.SqlTools.ServiceLayer.TaskServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility;
using Moq;
using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
{
public class TaskServiceTests : ServiceTestBase
{
private TaskService service;
private Mock<IProtocolEndpoint> serviceHostMock;
private TaskMetadata taskMetaData = new TaskMetadata
{
ServerName = "server name",
DatabaseName = "database name"
};
public TaskServiceTests()
{
serviceHostMock = new Mock<IProtocolEndpoint>();
service = CreateService();
service.InitializeService(serviceHostMock.Object);
}
[Fact]
public async Task TaskListRequestErrorsIfParameterIsNull()
{
object errorResponse = null;
var contextMock = RequestContextMocks.Create<ListTasksResponse>(null)
.AddErrorHandling((errorMessage, errorCode) => errorResponse = errorMessage);
await service.HandleListTasksRequest(null, contextMock.Object);
VerifyErrorSent(contextMock);
Assert.True(((string)errorResponse).Contains("ArgumentNullException"));
}
[Fact]
public void NewTaskShouldSendNotification()
{
serviceHostMock.AddEventHandling(TaskCreatedNotification.Type, null);
serviceHostMock.AddEventHandling(TaskStatusChangedNotification.Type, null);
DatabaseOperationStub operation = new DatabaseOperationStub();
SqlTask sqlTask = service.TaskManager.CreateTask(taskMetaData, operation.FunctionToRun);
sqlTask.Run().ContinueWith(task =>
{
});
serviceHostMock.Verify(x => x.SendEvent(TaskCreatedNotification.Type,
It.Is<TaskInfo>(t => t.TaskId == sqlTask.TaskId.ToString())), Times.Once());
operation.Stop();
serviceHostMock.Verify(x => x.SendEvent(TaskStatusChangedNotification.Type,
It.Is<TaskProgressInfo>(t => t.TaskId == sqlTask.TaskId.ToString())), Times.AtLeastOnce());
}
[Fact]
public async Task CancelTaskShouldCancelTheOperationAndSendNotification()
{
serviceHostMock.AddEventHandling(TaskCreatedNotification.Type, null);
serviceHostMock.AddEventHandling(TaskStatusChangedNotification.Type, null);
DatabaseOperationStub operation = new DatabaseOperationStub();
SqlTask sqlTask = service.TaskManager.CreateTask(taskMetaData, operation.FunctionToRun);
sqlTask.Run().ContinueWith(task =>
{
serviceHostMock.Verify(x => x.SendEvent(TaskStatusChangedNotification.Type,
It.Is<TaskProgressInfo>(t => t.Status == SqlTaskStatus.Canceled)), Times.Once());
});
CancelTaskParams cancelParams = new CancelTaskParams
{
TaskId = sqlTask.TaskId.ToString()
};
await RunAndVerify<bool>(
test: (requestContext) => service.HandleCancelTaskRequest(cancelParams, requestContext),
verify: ((result) =>
{
}));
serviceHostMock.Verify(x => x.SendEvent(TaskCreatedNotification.Type,
It.Is<TaskInfo>(t => t.TaskId == sqlTask.TaskId.ToString())), Times.Once());
}
[Fact]
public async Task TaskListTaskShouldReturnAllTasks()
{
serviceHostMock.AddEventHandling(TaskCreatedNotification.Type, null);
serviceHostMock.AddEventHandling(TaskStatusChangedNotification.Type, null);
DatabaseOperationStub operation = new DatabaseOperationStub();
SqlTask sqlTask = service.TaskManager.CreateTask(taskMetaData, operation.FunctionToRun);
sqlTask.Run();
ListTasksParams listParams = new ListTasksParams
{
};
await RunAndVerify<ListTasksResponse>(
test: (requestContext) => service.HandleListTasksRequest(listParams, requestContext),
verify: ((result) =>
{
Assert.True(result.Tasks.Any(x => x.TaskId == sqlTask.TaskId.ToString()));
}));
operation.Stop();
}
protected TaskService CreateService()
{
CreateServiceProviderWithMinServices();
// Create the service using the service provider, which will initialize dependencies
return ServiceProvider.GetService<TaskService>();
}
protected override RegisteredServiceProvider CreateServiceProviderWithMinServices()
{
return CreateProvider()
.RegisterSingleService(new TaskService());
}
}
}