Initial commit to new repository
This commit is contained in:
157
src/AxCopilot/Services/DraftQueueService.cs
Normal file
157
src/AxCopilot/Services/DraftQueueService.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 드래프트 큐 항목의 정렬과 상태 전이를 담당한다.
|
||||
/// 이후 command queue 실행기와 직접 연결될 수 있도록 queue policy를 분리한다.
|
||||
/// </summary>
|
||||
public sealed class DraftQueueService
|
||||
{
|
||||
public sealed class DraftQueueSummary
|
||||
{
|
||||
public int TotalCount { get; init; }
|
||||
public int QueuedCount { get; init; }
|
||||
public int RunningCount { get; init; }
|
||||
public int BlockedCount { get; init; }
|
||||
public int FailedCount { get; init; }
|
||||
public int CompletedCount { get; init; }
|
||||
public DraftQueueItem? NextItem { get; init; }
|
||||
public DateTime? NextReadyAt { get; init; }
|
||||
}
|
||||
|
||||
public DraftQueueItem CreateItem(string text, string priority = "next")
|
||||
{
|
||||
return new DraftQueueItem
|
||||
{
|
||||
Text = text.Trim(),
|
||||
Priority = NormalizePriority(priority),
|
||||
State = "queued",
|
||||
CreatedAt = DateTime.Now,
|
||||
};
|
||||
}
|
||||
|
||||
public DraftQueueItem? GetNextQueuedItem(IEnumerable<DraftQueueItem>? items, DateTime? now = null)
|
||||
{
|
||||
var at = now ?? DateTime.Now;
|
||||
return items?
|
||||
.Where(x => CanRunNow(x, at))
|
||||
.OrderBy(GetPriorityRank)
|
||||
.ThenBy(x => x.CreatedAt)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
public DraftQueueSummary GetSummary(IEnumerable<DraftQueueItem>? items, DateTime? now = null)
|
||||
{
|
||||
var at = now ?? DateTime.Now;
|
||||
var snapshot = items?.ToList() ?? [];
|
||||
var nextReadyAt = snapshot
|
||||
.Where(IsBlocked)
|
||||
.Select(x => x.NextRetryAt)
|
||||
.Where(x => x.HasValue)
|
||||
.OrderBy(x => x)
|
||||
.FirstOrDefault();
|
||||
|
||||
return new DraftQueueSummary
|
||||
{
|
||||
TotalCount = snapshot.Count,
|
||||
QueuedCount = snapshot.Count(x => CanRunNow(x, at)),
|
||||
RunningCount = snapshot.Count(x => string.Equals(x.State, "running", StringComparison.OrdinalIgnoreCase)),
|
||||
BlockedCount = snapshot.Count(IsBlocked),
|
||||
FailedCount = snapshot.Count(x => string.Equals(x.State, "failed", StringComparison.OrdinalIgnoreCase)),
|
||||
CompletedCount = snapshot.Count(x => string.Equals(x.State, "completed", StringComparison.OrdinalIgnoreCase)),
|
||||
NextItem = GetNextQueuedItem(snapshot, at),
|
||||
NextReadyAt = nextReadyAt,
|
||||
};
|
||||
}
|
||||
|
||||
public bool MarkRunning(DraftQueueItem? item)
|
||||
{
|
||||
if (item == null)
|
||||
return false;
|
||||
|
||||
item.State = "running";
|
||||
item.LastError = null;
|
||||
item.NextRetryAt = null;
|
||||
item.AttemptCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool MarkCompleted(DraftQueueItem? item)
|
||||
{
|
||||
if (item == null)
|
||||
return false;
|
||||
|
||||
item.State = "completed";
|
||||
item.LastError = null;
|
||||
item.NextRetryAt = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool MarkFailed(DraftQueueItem? item, string? error)
|
||||
{
|
||||
if (item == null)
|
||||
return false;
|
||||
|
||||
item.State = "failed";
|
||||
item.LastError = string.IsNullOrWhiteSpace(error) ? null : error.Trim();
|
||||
item.NextRetryAt = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ScheduleRetry(DraftQueueItem? item, string? error, TimeSpan? delay = null)
|
||||
{
|
||||
if (item == null)
|
||||
return false;
|
||||
|
||||
item.State = "queued";
|
||||
item.LastError = string.IsNullOrWhiteSpace(error) ? null : error.Trim();
|
||||
item.NextRetryAt = DateTime.Now.Add(delay ?? GetRetryDelay(item));
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ResetToQueued(DraftQueueItem? item)
|
||||
{
|
||||
if (item == null)
|
||||
return false;
|
||||
|
||||
item.State = "queued";
|
||||
item.LastError = null;
|
||||
item.NextRetryAt = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public TimeSpan GetRetryDelay(DraftQueueItem item)
|
||||
{
|
||||
var seconds = Math.Min(300, 15 * Math.Max(1, (int)Math.Pow(2, Math.Max(0, item.AttemptCount - 1))));
|
||||
return TimeSpan.FromSeconds(seconds);
|
||||
}
|
||||
|
||||
public bool CanRunNow(DraftQueueItem item, DateTime? now = null)
|
||||
{
|
||||
var at = now ?? DateTime.Now;
|
||||
return IsQueued(item) && (!item.NextRetryAt.HasValue || item.NextRetryAt.Value <= at);
|
||||
}
|
||||
|
||||
private static bool IsQueued(DraftQueueItem item)
|
||||
=> string.Equals(item.State, "queued", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsBlocked(DraftQueueItem item)
|
||||
=> IsQueued(item) && item.NextRetryAt.HasValue && item.NextRetryAt.Value > DateTime.Now;
|
||||
|
||||
private static string NormalizePriority(string? priority)
|
||||
=> priority switch
|
||||
{
|
||||
"now" => "now",
|
||||
"later" => "later",
|
||||
_ => "next",
|
||||
};
|
||||
|
||||
private static int GetPriorityRank(DraftQueueItem item)
|
||||
=> NormalizePriority(item.Priority) switch
|
||||
{
|
||||
"now" => 0,
|
||||
"next" => 1,
|
||||
_ => 2,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user