//
// 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
{
///
/// 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
///
public class SqlTask : IDisposable
{
private bool isCompleted;
private bool isCanceled;
private bool isDisposed;
private readonly object lockObject = new object();
private readonly List messages = new List();
private DateTime startTime;
private SqlTaskStatus status = SqlTaskStatus.NotStarted;
private DateTime stopTime;
public event EventHandler> MessageAdded;
public event EventHandler> StatusChanged;
public event EventHandler> TaskCanceled;
///
/// Creates new instance of SQL task
///
/// Task Metadata
/// The function to run to start the task
public SqlTask(TaskMetadata taskMetdata, Func> testToRun)
{
Validate.IsNotNull(nameof(taskMetdata), taskMetdata);
Validate.IsNotNull(nameof(testToRun), testToRun);
TaskMetadata = taskMetdata;
TaskToRun = testToRun;
StartTime = DateTime.UtcNow;
TaskId = Guid.NewGuid();
}
///
/// Task Metadata
///
internal TaskMetadata TaskMetadata { get; private set; }
///
/// The function to run
///
private Func> TaskToRun
{
get;
set;
}
///
/// Task unique id
///
public Guid TaskId { get; private set; }
///
/// Starts the task and monitor the task progress
///
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);
}
}
});
}
///
/// Returns true if task has any messages
///
public bool HasMessages
{
get
{
lock (lockObject)
{
return messages.Any();
}
}
}
///
/// Setting this to True will not change the Slot status.
/// Setting the Slot status to Canceled will set this to true.
///
public bool IsCanceled
{
get
{
return isCanceled;
}
private set
{
if (isCanceled != value)
{
isCanceled = value;
OnTaskCanceled();
}
}
}
///
/// Returns true if task is canceled, failed or succeed
///
public bool IsCompleted
{
get
{
return isCompleted;
}
private set
{
if (isCompleted != value)
{
isCompleted = value;
if (isCompleted)
{
StopTime = DateTime.UtcNow;
}
}
}
}
///
/// Task Messages
///
internal ReadOnlyCollection Messages
{
get
{
lock (lockObject)
{
return messages.AsReadOnly();
}
}
}
///
/// Start Time
///
public DateTime StartTime
{
get
{
return startTime;
}
internal set
{
startTime = value;
}
}
///
/// The total number of seconds to run the task
///
public double Duration
{
get
{
return (stopTime - startTime).TotalMilliseconds;
}
}
///
/// Task Status
///
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();
}
}
///
/// The date time that the task was complete
///
public DateTime StopTime
{
get
{
return stopTime;
}
internal set
{
stopTime = value;
}
}
///
/// 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
///
public void Cancel()
{
IsCanceled = true;
}
///
/// Adds a new message to the task messages
///
/// Message description
/// Status of the message
/// If true, the new messages will be added to the top. Default is false
///
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(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;
}
///
/// Converts the task to Task info to be used in the contracts
///
///
public TaskInfo ToTaskInfo()
{
return new TaskInfo
{
DatabaseName = TaskMetadata.DatabaseName,
ServerName = TaskMetadata.ServerName,
Name = TaskMetadata.Name,
Description = TaskMetadata.Description,
TaskId = TaskId.ToString()
};
}
///
/// 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.
///
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 e)
{
var handler = MessageAdded;
if (handler != null)
{
handler(this, e);
}
}
private void OnStatusChanged()
{
var handler = StatusChanged;
if (handler != null)
{
handler(this, new TaskEventArgs(TaskStatus, this));
}
}
private void OnTaskCanceled()
{
var handler = TaskCanceled;
if (handler != null)
{
handler(this, new TaskEventArgs(TaskStatus, this));
}
}
public void Dispose()
{
//Dispose
isDisposed = true;
}
protected void ValidateNotDisposed()
{
if (isDisposed)
{
throw new ObjectDisposedException(typeof(SqlTask).FullName);
}
}
///
/// Returns the most recently created message. Returns null if there are no messages on the slot.
///
public TaskMessage GetLastMessage()
{
ValidateNotDisposed();
lock (lockObject)
{
if (messages.Count > 0)
{
// get
return messages.Last();
}
}
return null;
}
}
}