feat(core,dal,domain,templateUpdater): Задания роботам выдаюстя с учетом подмены и множества переименований

This commit is contained in:
Mikhail Trubnikov
2026-07-29 16:31:50 +10:00
parent 7d5fb23daf
commit acdb6ec893
6 changed files with 4306 additions and 54 deletions

View File

@@ -260,15 +260,18 @@ namespace PARR.Core.Services.RobotTask.Implementations
if (tasks.Count == 0 || robotCode != RobotsEnum.TemplateOrder) if (tasks.Count == 0 || robotCode != RobotsEnum.TemplateOrder)
return tasks; return tasks;
_logger.LogDebug("Исходный пул задач для проверки переименования: {Tasks}",
string.Join(" | ", tasks.Select(t => $"[Id: {t.TaskId}, Name: '{t.TemplateName}']")));
// Ищем есть ли связанные шаблоны с таким имененм на переименование // Ищем есть ли связанные шаблоны с таким имененм на переименование
var taskTemplateNames = tasks.Select(t => t.TemplateName).Distinct().ToList(); var taskTemplateNames = tasks.Select(t => t.TemplateName).Distinct().ToList();
// Ищем записи в таблице переименований, где OldName совпадает с именами наших новых задач // Ищем записи в таблице переименований, где OldName совпадает с именами наших новых задач
var templatesToRename = await _templateRenamePendingRepository.Get().AsNoTracking() var templatesToRename = await _templateRenamePendingRepository.Get()
.AsNoTracking()
.Where(t => taskTemplateNames.Contains(t.OldName)) .Where(t => taskTemplateNames.Contains(t.OldName))
.ToListAsync(); .ToListAsync();
_logger.LogDebug("Найдено шаблонов в процессе переименования для текущих задач: {Count} шт.", templatesToRename.Count); _logger.LogDebug("Найдено записей в TemplateRenamePending для текущих задач: {Count} шт.", templatesToRename.Count);
if (templatesToRename.Count == 0) if (templatesToRename.Count == 0)
return tasks; return tasks;
@@ -288,7 +291,9 @@ namespace PARR.Core.Services.RobotTask.Implementations
&& t.TaskStatusCode == (int)TaskStatusEnum.Updating && t.TaskStatusCode == (int)TaskStatusEnum.Updating
).ToListAsync(); ).ToListAsync();
// --- Блок обработки ошибок --- // =========================================================================
// БЛОК 1: ОБРАБОТКА ОШИБОК (Правило: если ХОТЯ БЫ ОДНА упала в ошибку -> оригинал в ошибку)
// =========================================================================
// Если старый шаблон в ошибке и лимит попыток исчерпан, ставим ошибку и новому шаблону // Если старый шаблон в ошибке и лимит попыток исчерпан, ставим ошибку и новому шаблону
var errorTasks = renameTasks var errorTasks = renameTasks
@@ -300,16 +305,16 @@ namespace PARR.Core.Services.RobotTask.Implementations
var tasksToSetErrorStatus = new List<Guid>(); var tasksToSetErrorStatus = new List<Guid>();
if (errorTasks.Count > 0) if (errorTasks.Count > 0)
{ {
_logger.LogDebug("Найдено старых заданий на переименование с ошибками: {ErrorCount}. Ставим ошибку целевым (новым) заданиям.", errorTasks.Count); _logger.LogDebug("Найдено связанных заданий на переименование с ошибками: {ErrorCount}. Ставим ошибку целевым (новым) заданиям.", errorTasks.Count);
//var errorTemplateNames = errorTasks.Select(t => t.Template!.Name).ToHashSet(); // Собираем ВСЕ OldName, для которых есть хотя бы одна упавшая в ошибку задача.
// Берем OldName из словаря // Использование ToHashSet() гарантирует, что если 1 или 10 задач в ошибке, OldName попадет в набор один раз.
var errorOldNames = errorTasks var errorOldNames = errorTasks
.Where(t => templateIdToOldName.ContainsKey(t.TemplateId)) .Where(t => templateIdToOldName.ContainsKey(t.TemplateId))
.Select(t => templateIdToOldName[t.TemplateId]) .Select(t => templateIdToOldName[t.TemplateId])
.ToHashSet(); .ToHashSet();
// Берем целевые таски, находим в них задания которым надо поставить ошибку // Находим оригинальные задачи, чье имя совпадает с любым из "ошибочных" OldName
tasksToSetErrorStatus = tasks tasksToSetErrorStatus = tasks
.Where(t => errorOldNames.Contains(t.TemplateName)) .Where(t => errorOldNames.Contains(t.TemplateName))
.Select(t => t.TaskId) .Select(t => t.TaskId)
@@ -318,16 +323,17 @@ namespace PARR.Core.Services.RobotTask.Implementations
if (tasksToSetErrorStatus.Count > 0) if (tasksToSetErrorStatus.Count > 0)
{ {
// Устанавливаем ошибку целевым + пишем комментарий от робота + нажимаем комит // Устанавливаем ошибку целевым + пишем комментарий от робота + нажимаем комит
var logMessage = "[RobotTaskService] Установлен статус ошибки, так как не переименован связанный шаблон"; var logMessage = "[RobotTaskService] Установлен статус ошибки, так как хотя бы одна из связанных задач переименования не была успешно выполнена.";
await SetErrorStatusAsync(tasksToSetErrorStatus, logMessage); await SetErrorStatusAsync(tasksToSetErrorStatus, logMessage);
} }
} }
// --- Блок подмены задач --- // =========================================================================
// БЛОК 2: ПОДМЕНА ЗАДАЧ (Правило: берем ПЕРВУЮ валидную задачу для подмены)
// =========================================================================
var endDate = DateTimeOffset.UtcNow.Add(-_settingsFromDb.RobotWaitTime); var endDate = DateTimeOffset.UtcNow.Add(-_settingsFromDb.RobotWaitTime);
// Фильтруем старые задачи, которые МОЖНО взять в работу. Смотрим статусы роботов, можно взять в работу, только если (RobotStatus == Wait) или (InpRogress но которые еще не просрочены) // Фильтруем старые задачи, которые МОЖНО взять в работу. Смотрим статусы роботов, можно взять в работу, только если (RobotStatus == Wait) или (InProgress но которые еще не просрочены)
var allowedTasks = renameTasks.Where(t => var allowedTasks = renameTasks.Where(t =>
t.RobotStatusCode == (int)RobotStatusEnum.Wait t.RobotStatusCode == (int)RobotStatusEnum.Wait
|| (t.RobotStatusCode == (int)RobotStatusEnum.InProgress || (t.RobotStatusCode == (int)RobotStatusEnum.InProgress
@@ -347,7 +353,7 @@ namespace PARR.Core.Services.RobotTask.Implementations
.Select(t => new { OldName = templateIdToOldName[t.TemplateId], StatusTypeId = t.Template!.StatusTypeId }) .Select(t => new { OldName = templateIdToOldName[t.TemplateId], StatusTypeId = t.Template!.StatusTypeId })
.ToDictionary(x => x.OldName, x => x.StatusTypeId); .ToDictionary(x => x.OldName, x => x.StatusTypeId);
// 3. Оставляем для подмены только те задачи, у которых StatusTypeId == 0 // 3. Оставляем для подмены только те задачи, у которых StatusTypeId == Used
var validAllowedTasks = allowedTasks var validAllowedTasks = allowedTasks
.Where(t => t.Template != null && t.Template.StatusTypeId == TemplateStatusTypeEnum.Used) .Where(t => t.Template != null && t.Template.StatusTypeId == TemplateStatusTypeEnum.Used)
.ToList(); .ToList();
@@ -358,11 +364,13 @@ namespace PARR.Core.Services.RobotTask.Implementations
var errorCount = errorTaskIdsSet.Count; var errorCount = errorTaskIdsSet.Count;
// Создаем словарь подмены ТОЛЬКО из валидных задач (где StatusTypeId == Used) // Создаем словарь подмены ТОЛЬКО из валидных задач (где StatusTypeId == Used)
// ГРУППИРУЕМ по OldName и берем .First()!
// Это реализует правило: "если записей несколько, берем из них первую и подменяем ей оригинальное задание".
var renameTasksToDictionary = validAllowedTasks var renameTasksToDictionary = validAllowedTasks
.Where(t => templateIdToOldName.ContainsKey(t.TemplateId)) .Where(t => templateIdToOldName.ContainsKey(t.TemplateId))
.GroupBy(t => templateIdToOldName[t.TemplateId]) .GroupBy(t => templateIdToOldName[t.TemplateId])
.ToDictionary( .ToDictionary(
g => g.Key, g => g.Key, // Ключ = OldName
g => new RobotTaskDetails(g.First().Id, g.First().Template!.Name, g.First().Template!.NextRun) g => new RobotTaskDetails(g.First().Id, g.First().Template!.Name, g.First().Template!.NextRun)
); );
@@ -393,8 +401,13 @@ namespace PARR.Core.Services.RobotTask.Implementations
} }
// 3. Если для этого имени шаблона есть разрешенная задача на переименование (и она валидна) - вставляем ее // 3. Если для этого имени шаблона есть разрешенная задача на переименование (и она валидна) - вставляем ее
// Подменяем оригинальную задачу на ПЕРВУЮ валидную задачу переименования
if (renameTasksToDictionary.TryGetValue(task.TemplateName, out var renameTask)) if (renameTasksToDictionary.TryGetValue(task.TemplateName, out var renameTask))
{ {
_logger.LogDebug( "ПОДМЕНА ЗАДАЧИ: Исходная [Id: {OriginalId}, Name: '{OriginalName}'] " +
"-> Заменена на [Id: {NewId}, Name: '{NewName}']",
task.TaskId, task.TemplateName, renameTask.TaskId, renameTask.TemplateName);
finalTasks.Add(renameTask); finalTasks.Add(renameTask);
replacedCount++; replacedCount++;
} }
@@ -405,11 +418,13 @@ namespace PARR.Core.Services.RobotTask.Implementations
} }
} }
// Логируем итоговую статистику трансформации пула задач _logger.LogDebug("Итоговый пул задач после трансформации: {Tasks}",
string.Join(" | ", finalTasks.Select(t => $"[Id: {t.TaskId}, Name: '{t.TemplateName}']")));
_logger.LogInformation( _logger.LogInformation(
"Трансформация пула задач завершена. Исходных: {OriginalCount} шт. " + "Трансформация пула задач завершена. Исходных: {OriginalCount} шт. " +
"Отклонено (ошибка): {ErrorCount} шт. Исключено (невалидный StatusTypeId): {ExcludedCount} шт. " + "Отклонено (ошибка): {ErrorCount} шт. Исключено (невалидный StatusTypeId): {ExcludedCount} шт. " +
"Заменено на старые: {ReplacedCount} шт. Итого к выдаче: {FinalCount} шт.", "Заменено на старые (взята первая из группы): {ReplacedCount} шт. Итого к выдаче: {FinalCount} шт.",
originalCount, errorCount, excludedByStatusCount, replacedCount, finalTasks.Count); originalCount, errorCount, excludedByStatusCount, replacedCount, finalTasks.Count);
// Возвращаем без дополнительной сортировки по NextRun. Порядок сохранен начального списка // Возвращаем без дополнительной сортировки по NextRun. Порядок сохранен начального списка

View File

@@ -0,0 +1,54 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PARR.DAL.Migrations
{
/// <inheritdoc />
public partial class tblTemplateRenamePendingAddDateModifiedRemUniqueIndex : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_TemplateRenamePendings_OldName",
schema: "template",
table: "TemplateRenamePendings");
migrationBuilder.AddColumn<DateTimeOffset>(
name: "DateModified",
schema: "template",
table: "TemplateRenamePendings",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_TemplateRenamePendings_OldName",
schema: "template",
table: "TemplateRenamePendings",
column: "OldName");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_TemplateRenamePendings_OldName",
schema: "template",
table: "TemplateRenamePendings");
migrationBuilder.DropColumn(
name: "DateModified",
schema: "template",
table: "TemplateRenamePendings");
migrationBuilder.CreateIndex(
name: "IX_TemplateRenamePendings_OldName",
schema: "template",
table: "TemplateRenamePendings",
column: "OldName",
unique: true);
}
}
}

View File

@@ -2860,6 +2860,9 @@ namespace PARR.DAL.Migrations
b.Property<DateTimeOffset>("DateCreated") b.Property<DateTimeOffset>("DateCreated")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<DateTimeOffset?>("DateModified")
.HasColumnType("timestamp with time zone");
b.Property<string>("OldName") b.Property<string>("OldName")
.IsRequired() .IsRequired()
.HasColumnType("text") .HasColumnType("text")
@@ -2867,8 +2870,7 @@ namespace PARR.DAL.Migrations
b.HasKey("TemplateId"); b.HasKey("TemplateId");
b.HasIndex("OldName") b.HasIndex("OldName");
.IsUnique();
b.ToTable("TemplateRenamePendings", "template", t => b.ToTable("TemplateRenamePendings", "template", t =>
{ {

View File

@@ -10,7 +10,8 @@ namespace PARR.Domain.Entities.TemplateEntities
/// </summary> /// </summary>
[Table("TemplateRenamePendings", Schema = DatabaseSchemas.Template)] [Table("TemplateRenamePendings", Schema = DatabaseSchemas.Template)]
[Comment("Шаблоны находящиеся в процессе переименования")] [Comment("Шаблоны находящиеся в процессе переименования")]
[Index(nameof(OldName), IsUnique = true)] //[Index(nameof(OldName), IsUnique = true)]
[Index(nameof(OldName))]
public class TemplateRenamePending public class TemplateRenamePending
{ {
[Key] [Key]
@@ -18,6 +19,8 @@ namespace PARR.Domain.Entities.TemplateEntities
public DateTimeOffset DateCreated { get; set; } public DateTimeOffset DateCreated { get; set; }
public DateTimeOffset? DateModified { get; set; }
/// <summary> /// <summary>
/// Старое имя шаблона /// Старое имя шаблона
/// </summary> /// </summary>
@@ -26,6 +29,6 @@ namespace PARR.Domain.Entities.TemplateEntities
[ForeignKey(nameof(TemplateId))] [ForeignKey(nameof(TemplateId))]
public required Template Template { get; set; } public Template? Template { get; set; }
} }
} }

View File

@@ -299,46 +299,87 @@ namespace PARR.TemplateUpdater.Services
/// <returns></returns> /// <returns></returns>
private async Task<bool> PrepareOldTemplateNameAsync(string oldName, string newName, Template template) private async Task<bool> PrepareOldTemplateNameAsync(string oldName, string newName, Template template)
{ {
// Проверяем, не запущено ли уже переименование для этого шаблона var existRenamePending = await _templateRenamePendingRepository.Get()
var alreadyPending = await _templateRenamePendingRepository.Get()
.AsNoTracking()
.FirstOrDefaultAsync(t => t.TemplateId == template.Id); .FirstOrDefaultAsync(t => t.TemplateId == template.Id);
if (alreadyPending != null) if (existRenamePending != null)
{ {
logger.LogError("При попытке переименования шаблона {TemplateId}, из '{OldName}' в '{NewName}', " + logger.LogInformation(
"произошла ошибка, этот шаблон уже находится в процессе переименования (старое имя {PendingName})", template.Id, oldName, newName, alreadyPending.OldName); "Найдено существующее задание на переименование шаблона {TemplateId}. " +
return false; "Обновляю OldName с '{OriginalOldName}' на '{NewOldName}'.",
template.Id, existRenamePending.OldName, oldName);
// Подменяем имя шаблона
existRenamePending.OldName = oldName;
existRenamePending.DateModified = DateTimeOffset.UtcNow;
return true;
} }
else
// Уникально ли имя в таблице ожидания переименования
var existPendingOldName = await _templateRenamePendingRepository.Get()
.AsNoTracking()
.FirstOrDefaultAsync(t => t.OldName == oldName);
if (existPendingOldName != null)
{ {
logger.LogError("При добавлении старого имени в таблицу ожидания для шаблона {TemplateId} обнаружен конфликт: " + // Создаем запись
"имя '{ExistOldName}' уже зарезервировано другим процессом для шаблона {ExistTemplateId}",
template.Id, existPendingOldName.OldName, existPendingOldName.TemplateId);
return false;
}
// Все нормально, добавляем запись в таблицу
var pendingRename = new TemplateRenamePending var pendingRename = new TemplateRenamePending
{ {
TemplateId = template.Id, TemplateId = template.Id,
DateCreated = DateTimeOffset.UtcNow, DateCreated = DateTimeOffset.UtcNow,
OldName = oldName, OldName = oldName
Template = template
}; };
logger.LogInformation(
"Добавлен шаблон в таблицу ожидания переименования. TemplateId: {TemplateId}, OldName: '{OldName}', NewName: '{NewName}'.",
template.Id, oldName, newName);
var addResult = await _templateRenamePendingRepository.CreateAsync(pendingRename); var addResult = await _templateRenamePendingRepository.CreateAsync(pendingRename);
if (!addResult) if (!addResult)
return false; return false;
return true; return true;
} }
#region Old
//// Проверяем, не запущено ли уже переименование для этого шаблона
//var alreadyPending = await _templateRenamePendingRepository.Get()
// .AsNoTracking()
// .FirstOrDefaultAsync(t => t.TemplateId == template.Id);
//if (alreadyPending != null)
//{
// logger.LogError("При попытке переименования шаблона {TemplateId}, из '{OldName}' в '{NewName}', " +
// "произошла ошибка, этот шаблон уже находится в процессе переименования (старое имя {PendingName})", template.Id, oldName, newName, alreadyPending.OldName);
// return false;
//}
//// Уникально ли имя в таблице ожидания переименования
//var existPendingOldName = await _templateRenamePendingRepository.Get()
// .AsNoTracking()
// .FirstOrDefaultAsync(t => t.OldName == oldName);
//if (existPendingOldName != null)
//{
// logger.LogError("При добавлении старого имени в таблицу ожидания для шаблона {TemplateId} обнаружен конфликт: " +
// "имя '{ExistOldName}' уже зарезервировано другим процессом для шаблона {ExistTemplateId}",
// template.Id, existPendingOldName.OldName, existPendingOldName.TemplateId);
// return false;
//}
//// Все нормально, добавляем запись в таблицу
//var pendingRename = new TemplateRenamePending
//{
// TemplateId = template.Id,
// DateCreated = DateTimeOffset.UtcNow,
// OldName = oldName,
// Template = template
//};
//var addResult = await _templateRenamePendingRepository.CreateAsync(pendingRename);
//if (!addResult)
// return false;
//return true;
#endregion
}
} }
} }