Update scheduling parameters and refactor channel endpoints: extend HorizonDays to 7 and RetentionDays to 90 in appsettings.json. Consolidate channel-related endpoint logic by removing obsolete files and enhancing the ShowEndpoints with audience and genre management capabilities. Improve error handling and streamline command handlers for channel operations.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
|
||||
namespace TeleWave.Infrastructure.Broadcast;
|
||||
|
||||
/// <summary>
|
||||
/// Обслуживание, которое раньше не делалось вовсе: чистка отрендеренных заставок, на которые больше
|
||||
/// никто не ссылается. Они копятся при каждой смене пары шоу и при каждой правке блока заставки,
|
||||
/// а после перехода на длинное хранение расписания их станет заметно больше.
|
||||
///
|
||||
/// Идёт по расписанию отдельно от генерации: та держит advisory-лок канала, и подмешивать в неё
|
||||
/// удаление по всей таблице не нужно.
|
||||
/// </summary>
|
||||
public sealed class MaintenanceBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<MaintenanceBackgroundService> logger
|
||||
) : BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan Interval = TimeSpan.FromHours(6);
|
||||
private static readonly TimeSpan StartupDelay = TimeSpan.FromMinutes(2);
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(StartupDelay, stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var timer = new PeriodicTimer(Interval);
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
await TickAsync(stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Ошибка обслуживания");
|
||||
}
|
||||
} while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
}
|
||||
|
||||
private async Task TickAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
|
||||
|
||||
// Осиротевшей считается заставка, чей ассет не встречается ни в одной записи расписания.
|
||||
// Границу по времени не ставим: окно хранения расписания уже определяет, что живо.
|
||||
var removed = await db
|
||||
.BumperAssets.Where(b =>
|
||||
!db.ScheduleEntries.Any(e => e.MediaAssetId == b.MediaAssetId)
|
||||
)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
if (removed > 0)
|
||||
logger.LogInformation("Обслуживание: удалено осиротевших заставок — {Count}", removed);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +1,96 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TeleWave.Application.Broadcast.Scheduling;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
|
||||
namespace TeleWave.Infrastructure.Broadcast;
|
||||
|
||||
/// <summary>
|
||||
/// Периодически достраивает расписание каждого включённого канала до горизонта. Расширение хвоста —
|
||||
/// без удаления существующих записей, поэтому эфир не «дёргается».
|
||||
/// </summary>
|
||||
public sealed class SchedulingBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<SchedulerOptions> options,
|
||||
ILogger<SchedulingBackgroundService> logger
|
||||
) : BackgroundService
|
||||
{
|
||||
private readonly SchedulerOptions _options = options.Value;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Небольшая задержка на старте — дать примениться миграциям/сидингу.
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var timer = new PeriodicTimer(
|
||||
TimeSpan.FromMinutes(Math.Max(1, _options.TickMinutes))
|
||||
);
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
await TickAsync(stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Ошибка тика планировщика");
|
||||
}
|
||||
} while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
}
|
||||
|
||||
private async Task TickAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
|
||||
var generator = scope.ServiceProvider.GetRequiredService<ScheduleGenerator>();
|
||||
|
||||
var channelIds = await db
|
||||
.Channels.Where(c => c.IsEnabled)
|
||||
.Select(c => c.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
foreach (var channelId in channelIds)
|
||||
{
|
||||
var added = await generator.GenerateAsync(
|
||||
channelId,
|
||||
now,
|
||||
regenerate: false,
|
||||
cancellationToken
|
||||
);
|
||||
if (added > 0)
|
||||
logger.LogInformation(
|
||||
"Канал {ChannelId}: добавлено {Count} записей расписания",
|
||||
channelId,
|
||||
added
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TeleWave.Application.Broadcast.Scheduling;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Programming.Planning;
|
||||
|
||||
namespace TeleWave.Infrastructure.Broadcast;
|
||||
|
||||
/// <summary>
|
||||
/// Периодически достраивает расписание каждого включённого канала до горизонта. Расширение хвоста —
|
||||
/// без удаления существующих записей, поэтому эфир не «дёргается».
|
||||
/// </summary>
|
||||
public sealed class SchedulingBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<SchedulerOptions> options,
|
||||
ILogger<SchedulingBackgroundService> logger
|
||||
) : BackgroundService
|
||||
{
|
||||
private readonly SchedulerOptions _options = options.Value;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Небольшая задержка на старте — дать примениться миграциям/сидингу.
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var timer = new PeriodicTimer(
|
||||
TimeSpan.FromMinutes(Math.Max(1, _options.TickMinutes))
|
||||
);
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
await TickAsync(stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Ошибка тика планировщика");
|
||||
}
|
||||
} while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
}
|
||||
|
||||
private async Task TickAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
|
||||
var generator = scope.ServiceProvider.GetRequiredService<GridScheduleGenerator>();
|
||||
|
||||
// Эфир строит только шаблон сетки: канал без шаблона вещать не может и молча пропускается.
|
||||
var channelIds = await db
|
||||
.Channels.Where(c => c.IsEnabled && c.TemplateId != null)
|
||||
.Select(c => c.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
foreach (var channelId in channelIds)
|
||||
{
|
||||
var report = await generator.GenerateAsync(
|
||||
channelId,
|
||||
now,
|
||||
rebuildFuture: false,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (report.Added > 0)
|
||||
logger.LogInformation(
|
||||
"Канал {ChannelId}: добавлено {Count} записей расписания",
|
||||
channelId,
|
||||
report.Added
|
||||
);
|
||||
|
||||
foreach (var warning in report.Warnings)
|
||||
logger.LogWarning(
|
||||
"Канал {ChannelId}, слот {SlotId}: {Warning} — {Details}",
|
||||
channelId,
|
||||
warning.SlotId,
|
||||
warning.Kind,
|
||||
warning.Details
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user