feat(pipeline): render queue, worker service and automated preview generation
One "Queue Export" click now renders the EXR sequence, then rebuilds the shot headlessly with the studio slate/overlay template to produce the delivery MOV and review MP4. Implements RenderPipeline2 phases 1-2 plus the preview stage. Server: - New models Export, RenderJob, ExportEvent, Machine, WorkerHeartbeat, plus Project.deliveryConfig and per-submission slate fields (Export.vfxScope, Export.submissionNote, inherited from the shot's previous export). Both migrations are purely additive; no existing column is touched. - lib/render-pipeline: server-enforced state machine, transactional version increment with supersede, atomic FOR UPDATE SKIP LOCKED claim gated by machine availability windows, and a lease reaper run from instrumentation.ts. - /api/ext/* endpoints for the panel and workers; session-auth mirrors under /api/render and /api/machines for the web UI. - Pipeline pages: render queue, export detail, machine monitoring, plus an Exports tab on shot detail. RenderWorker (.NET 8 Windows service, new): - Registration, heartbeat as cancel channel, claim loop, aerender runner with progress parsing and stall watchdog, crash recovery and disk-spooled reporting that survives server downtime. - Preview stage: headless AE assembles the preview comp into a throwaway AEP with both output modules queued, then a single aerender pass renders them. Preview jobs are not claimed while an interactive AE session is open, so an artist's project is never taken over. AE panel: Queue Export with live status polling, urgent flag, retry, and the VFX Scope / Submission Note fields. Every existing panel action is unchanged. Preview chaining ships disabled behind SystemConfig preview.enabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace VFXReviewWorker;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal rolling file logger (§7.9): one file per day under
|
||||
/// %ProgramData%\VFXReviewWorker\logs, files older than 14 days pruned on
|
||||
/// startup and at midnight rollover. No external logging dependencies.
|
||||
/// </summary>
|
||||
public sealed class FileLoggerProvider : ILoggerProvider
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private StreamWriter? _writer;
|
||||
private DateOnly _currentDay;
|
||||
|
||||
public FileLoggerProvider()
|
||||
{
|
||||
Directory.CreateDirectory(WorkerOptions.LogsDir);
|
||||
Prune();
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName) => new FileLogger(this, categoryName);
|
||||
|
||||
internal void Write(string category, LogLevel level, string message, Exception? ex)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Now);
|
||||
if (_writer is null || today != _currentDay)
|
||||
{
|
||||
_writer?.Dispose();
|
||||
_currentDay = today;
|
||||
_writer = new StreamWriter(
|
||||
Path.Combine(WorkerOptions.LogsDir, $"worker_{today:yyyyMMdd}.log"),
|
||||
append: true) { AutoFlush = true };
|
||||
Prune();
|
||||
}
|
||||
_writer.WriteLine($"{DateTime.Now:HH:mm:ss.fff} [{level,-5}] {Shorten(category)}: {message}{(ex is null ? "" : $"\n{ex}")}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string Shorten(string category) => category[(category.LastIndexOf('.') + 1)..];
|
||||
|
||||
private static void Prune()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cutoff = DateTime.Now.AddDays(-14);
|
||||
foreach (var f in Directory.EnumerateFiles(WorkerOptions.LogsDir, "*.log"))
|
||||
{
|
||||
if (File.GetLastWriteTime(f) < cutoff) File.Delete(f);
|
||||
}
|
||||
}
|
||||
catch { /* best effort */ }
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_lock) { _writer?.Dispose(); _writer = null; }
|
||||
}
|
||||
|
||||
private sealed class FileLogger : ILogger
|
||||
{
|
||||
private readonly FileLoggerProvider _provider;
|
||||
private readonly string _category;
|
||||
|
||||
public FileLogger(FileLoggerProvider provider, string category)
|
||||
{
|
||||
_provider = provider;
|
||||
_category = category;
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Information;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel)) return;
|
||||
_provider.Write(_category, logLevel, formatter(state, exception), exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user