using DysonNetwork.Drive.Storage.Model; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Proto; using Microsoft.EntityFrameworkCore; using NanoidDotNet; using NodaTime; using TaskStatus = DysonNetwork.Drive.Storage.Model.TaskStatus; namespace DysonNetwork.Drive.Storage; /// /// Generic task service for handling various types of background operations /// public class PersistentTaskService( AppDatabase db, ICacheService cache, ILogger logger, RingService.RingServiceClient ringService ) { private const string CacheKeyPrefix = "task:"; private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30); /// /// Creates a new task of any type /// public async Task CreateTaskAsync(T task) where T : PersistentTask { task.TaskId = NanoidDotNet.Nanoid.Generate(); var now = SystemClock.Instance.GetCurrentInstant(); task.CreatedAt = now; task.UpdatedAt = now; task.LastActivity = now; task.StartedAt = now; db.Tasks.Add(task); await db.SaveChangesAsync(); await SetCacheAsync(task); await SendTaskCreatedNotificationAsync(task); return task; } /// /// Gets a task by ID /// public async Task GetTaskAsync(string taskId) where T : PersistentTask { var cacheKey = $"{CacheKeyPrefix}{taskId}"; var cachedTask = await cache.GetAsync(cacheKey); if (cachedTask is not null) return cachedTask; var task = await db.Tasks .FirstOrDefaultAsync(t => t.TaskId == taskId); if (task is T typedTask) { await SetCacheAsync(typedTask); return typedTask; } return null; } /// /// Updates task progress /// public async Task UpdateTaskProgressAsync(string taskId, double progress, string? statusMessage = null) { var task = await GetTaskAsync(taskId); if (task is null) return; var previousProgress = task.Progress; task.Progress = Math.Clamp(progress, 0, 100); task.LastActivity = SystemClock.Instance.GetCurrentInstant(); task.UpdatedAt = task.LastActivity; if (statusMessage is not null) { task.Description = statusMessage; } await db.SaveChangesAsync(); await SetCacheAsync(task); // Send progress update notification await SendTaskProgressUpdateAsync(task, task.Progress, previousProgress); } /// /// Marks a task as completed /// public async Task MarkTaskCompletedAsync(string taskId, Dictionary? results = null) { var task = await GetTaskAsync(taskId); if (task is null) return; var now = SystemClock.Instance.GetCurrentInstant(); task.Status = TaskStatus.Completed; task.Progress = 100; task.CompletedAt = now; task.LastActivity = now; task.UpdatedAt = now; if (results is not null) { foreach (var (key, value) in results) { task.Results[key] = value; } } await db.SaveChangesAsync(); await RemoveCacheAsync(taskId); await SendTaskCompletedNotificationAsync(task); } /// /// Marks a task as failed /// public async Task MarkTaskFailedAsync(string taskId, string? errorMessage = null) { var task = await GetTaskAsync(taskId); if (task is null) return; task.Status = TaskStatus.Failed; task.ErrorMessage = errorMessage ?? "Task failed due to an unknown error"; task.LastActivity = SystemClock.Instance.GetCurrentInstant(); task.UpdatedAt = task.LastActivity; await db.SaveChangesAsync(); await RemoveCacheAsync(taskId); await SendTaskFailedNotificationAsync(task); } /// /// Pauses a task /// public async Task PauseTaskAsync(string taskId) { var task = await GetTaskAsync(taskId); if (task is null || task.Status != TaskStatus.InProgress) return; task.Status = TaskStatus.Paused; task.LastActivity = SystemClock.Instance.GetCurrentInstant(); task.UpdatedAt = task.LastActivity; await db.SaveChangesAsync(); await SetCacheAsync(task); } /// /// Resumes a paused task /// public async Task ResumeTaskAsync(string taskId) { var task = await GetTaskAsync(taskId); if (task is null || task.Status != TaskStatus.Paused) return; task.Status = TaskStatus.InProgress; task.LastActivity = SystemClock.Instance.GetCurrentInstant(); task.UpdatedAt = task.LastActivity; await db.SaveChangesAsync(); await SetCacheAsync(task); } /// /// Cancels a task /// public async Task CancelTaskAsync(string taskId) { var task = await GetTaskAsync(taskId); if (task is null) return; task.Status = TaskStatus.Cancelled; task.LastActivity = SystemClock.Instance.GetCurrentInstant(); task.UpdatedAt = task.LastActivity; await db.SaveChangesAsync(); await RemoveCacheAsync(taskId); } /// /// Gets tasks for a user with filtering and pagination /// public async Task<(List Items, int TotalCount)> GetUserTasksAsync( Guid accountId, TaskType? type = null, TaskStatus? status = null, string? sortBy = "lastActivity", bool sortDescending = true, int offset = 0, int limit = 50 ) { var query = db.Tasks.Where(t => t.AccountId == accountId); // Apply filters if (type.HasValue) { query = query.Where(t => t.Type == type.Value); } if (status.HasValue) { query = query.Where(t => t.Status == status.Value); } // Get total count var totalCount = await query.CountAsync(); // Apply sorting IOrderedQueryable orderedQuery; switch (sortBy?.ToLower()) { case "name": orderedQuery = sortDescending ? query.OrderByDescending(t => t.Name) : query.OrderBy(t => t.Name); break; case "type": orderedQuery = sortDescending ? query.OrderByDescending(t => t.Type) : query.OrderBy(t => t.Type); break; case "progress": orderedQuery = sortDescending ? query.OrderByDescending(t => t.Progress) : query.OrderBy(t => t.Progress); break; case "createdat": orderedQuery = sortDescending ? query.OrderByDescending(t => t.CreatedAt) : query.OrderBy(t => t.CreatedAt); break; case "updatedat": orderedQuery = sortDescending ? query.OrderByDescending(t => t.UpdatedAt) : query.OrderBy(t => t.UpdatedAt); break; case "lastactivity": default: orderedQuery = sortDescending ? query.OrderByDescending(t => t.LastActivity) : query.OrderBy(t => t.LastActivity); break; } // Apply pagination var items = await orderedQuery .Skip(offset) .Take(limit) .ToListAsync(); return (items, totalCount); } /// /// Gets task statistics for a user /// public async Task GetUserTaskStatsAsync(Guid accountId) { var tasks = await db.Tasks .Where(t => t.AccountId == accountId) .ToListAsync(); var stats = new TaskStatistics { TotalTasks = tasks.Count, PendingTasks = tasks.Count(t => t.Status == TaskStatus.Pending), InProgressTasks = tasks.Count(t => t.Status == TaskStatus.InProgress), PausedTasks = tasks.Count(t => t.Status == TaskStatus.Paused), CompletedTasks = tasks.Count(t => t.Status == TaskStatus.Completed), FailedTasks = tasks.Count(t => t.Status == TaskStatus.Failed), CancelledTasks = tasks.Count(t => t.Status == TaskStatus.Cancelled), ExpiredTasks = tasks.Count(t => t.Status == TaskStatus.Expired), AverageProgress = tasks.Any(t => t.Status == TaskStatus.InProgress || t.Status == TaskStatus.Paused) ? tasks.Where(t => t.Status == TaskStatus.InProgress || t.Status == TaskStatus.Paused) .Average(t => t.Progress) : 0, RecentActivity = tasks.OrderByDescending(t => t.LastActivity) .Take(10) .Select(t => new TaskActivity { TaskId = t.TaskId, Name = t.Name, Type = t.Type, Status = t.Status, Progress = t.Progress, LastActivity = t.LastActivity }) .ToList() }; return stats; } /// /// Cleans up old completed/failed tasks /// public async Task CleanupOldTasksAsync(Guid accountId, Duration maxAge = default) { if (maxAge == default) { maxAge = Duration.FromDays(30); // Default 30 days } var cutoff = SystemClock.Instance.GetCurrentInstant() - maxAge; var oldTasks = await db.Tasks .Where(t => t.AccountId == accountId && (t.Status == TaskStatus.Completed || t.Status == TaskStatus.Failed || t.Status == TaskStatus.Cancelled || t.Status == TaskStatus.Expired) && t.UpdatedAt < cutoff) .ToListAsync(); db.Tasks.RemoveRange(oldTasks); await db.SaveChangesAsync(); // Clean up cache foreach (var task in oldTasks) { await RemoveCacheAsync(task.TaskId); } return oldTasks.Count; } #region Notification Methods private async Task SendTaskCreatedNotificationAsync(PersistentTask task) { try { var data = new TaskCreatedData { TaskId = task.TaskId, Name = task.Name, Type = task.Type.ToString(), CreatedAt = task.CreatedAt.ToString("O", null) }; var packet = new WebSocketPacket { Type = "task.created", Data = Google.Protobuf.ByteString.CopyFromUtf8(System.Text.Json.JsonSerializer.Serialize(data)) }; await ringService.PushWebSocketPacketAsync(new PushWebSocketPacketRequest { UserId = task.AccountId.ToString(), Packet = packet }); } catch (Exception ex) { logger.LogWarning(ex, "Failed to send task created notification for task {TaskId}", task.TaskId); } } private async Task SendTaskProgressUpdateAsync(PersistentTask task, double newProgress, double previousProgress) { try { // Only send significant progress updates (every 5% or major milestones) if (Math.Abs(newProgress - previousProgress) < 5 && newProgress < 100 && newProgress > 0) return; var data = new TaskProgressData { TaskId = task.TaskId, Name = task.Name, Type = task.Type.ToString(), Progress = newProgress, Status = task.Status.ToString(), LastActivity = task.LastActivity.ToString("O", null) }; var packet = new WebSocketPacket { Type = "task.progress", Data = Google.Protobuf.ByteString.CopyFromUtf8(System.Text.Json.JsonSerializer.Serialize(data)) }; await ringService.PushWebSocketPacketAsync(new PushWebSocketPacketRequest { UserId = task.AccountId.ToString(), Packet = packet }); } catch (Exception ex) { logger.LogWarning(ex, "Failed to send task progress update for task {TaskId}", task.TaskId); } } private async Task SendTaskCompletedNotificationAsync(PersistentTask task) { try { var data = new TaskCompletionData { TaskId = task.TaskId, Name = task.Name, Type = task.Type.ToString(), CompletedAt = task.CompletedAt?.ToString("O", null) ?? task.UpdatedAt.ToString("O", null), Results = task.Results }; // WebSocket notification var wsPacket = new WebSocketPacket { Type = "task.completed", Data = Google.Protobuf.ByteString.CopyFromUtf8(System.Text.Json.JsonSerializer.Serialize(data)) }; await ringService.PushWebSocketPacketAsync(new PushWebSocketPacketRequest { UserId = task.AccountId.ToString(), Packet = wsPacket }); // Push notification var pushNotification = new PushNotification { Topic = "task", Title = "Task Completed", Subtitle = task.Name, Body = $"Your {task.Type.ToString().ToLower()} task has completed successfully.", IsSavable = true }; await ringService.SendPushNotificationToUserAsync(new SendPushNotificationToUserRequest { UserId = task.AccountId.ToString(), Notification = pushNotification }); } catch (Exception ex) { logger.LogWarning(ex, "Failed to send task completion notification for task {TaskId}", task.TaskId); } } private async Task SendTaskFailedNotificationAsync(PersistentTask task) { try { var data = new TaskFailureData { TaskId = task.TaskId, Name = task.Name, Type = task.Type.ToString(), FailedAt = task.UpdatedAt.ToString("O", null), ErrorMessage = task.ErrorMessage ?? "Task failed due to an unknown error" }; // WebSocket notification var wsPacket = new WebSocketPacket { Type = "task.failed", Data = Google.Protobuf.ByteString.CopyFromUtf8(System.Text.Json.JsonSerializer.Serialize(data)) }; await ringService.PushWebSocketPacketAsync(new PushWebSocketPacketRequest { UserId = task.AccountId.ToString(), Packet = wsPacket }); // Push notification var pushNotification = new PushNotification { Topic = "task", Title = "Task Failed", Subtitle = task.Name, Body = $"Your {task.Type.ToString().ToLower()} task has failed.", IsSavable = true }; await ringService.SendPushNotificationToUserAsync(new SendPushNotificationToUserRequest { UserId = task.AccountId.ToString(), Notification = pushNotification }); } catch (Exception ex) { logger.LogWarning(ex, "Failed to send task failure notification for task {TaskId}", task.TaskId); } } #endregion #region Cache Methods private async Task SetCacheAsync(PersistentTask task) { var cacheKey = $"{CacheKeyPrefix}{task.TaskId}"; await cache.SetAsync(cacheKey, task, CacheDuration); } private async Task RemoveCacheAsync(string taskId) { var cacheKey = $"{CacheKeyPrefix}{taskId}"; await cache.RemoveAsync(cacheKey); } #endregion } #region Data Transfer Objects public class TaskCreatedData { public string TaskId { get; set; } = null!; public string Name { get; set; } = null!; public string Type { get; set; } = null!; public string CreatedAt { get; set; } = null!; } public class TaskProgressData { public string TaskId { get; set; } = null!; public string Name { get; set; } = null!; public string Type { get; set; } = null!; public double Progress { get; set; } public string Status { get; set; } = null!; public string LastActivity { get; set; } = null!; } public class TaskCompletionData { public string TaskId { get; set; } = null!; public string Name { get; set; } = null!; public string Type { get; set; } = null!; public string CompletedAt { get; set; } = null!; public Dictionary Results { get; set; } = new(); } public class TaskFailureData { public string TaskId { get; set; } = null!; public string Name { get; set; } = null!; public string Type { get; set; } = null!; public string FailedAt { get; set; } = null!; public string ErrorMessage { get; set; } = null!; } public class TaskStatistics { public int TotalTasks { get; set; } public int PendingTasks { get; set; } public int InProgressTasks { get; set; } public int PausedTasks { get; set; } public int CompletedTasks { get; set; } public int FailedTasks { get; set; } public int CancelledTasks { get; set; } public int ExpiredTasks { get; set; } public double AverageProgress { get; set; } public List RecentActivity { get; set; } = new(); } public class TaskActivity { public string TaskId { get; set; } = null!; public string Name { get; set; } = null!; public TaskType Type { get; set; } public TaskStatus Status { get; set; } public double Progress { get; set; } public Instant LastActivity { get; set; } } #endregion