477aef7b27
Deploy / deploy (push) Successful in 3m25s
Preview jobs looked stuck after aerender finished: the worker was silently
uploading the review MP4s and the ProRes MOV over a slow uplink, and a
restart mid-upload threw the preview away.
- Worker 1.7.1 replaces the job's log tail with live upload status and logs
each upload's size, duration and speed.
- The ProRes MOV now uploads after finalize via a persistent queue
(HighResUploader, resumes after restart, 5 attempts) and attaches through
the new E13b endpoint POST /api/ext/render/jobs/{id}/attach-highres.
A MOV from a superseded preview is discarded.
Deploy the server before the 1.7.1 worker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
3.6 KiB
C#
104 lines
3.6 KiB
C#
using System.Diagnostics;
|
|
using System.Text;
|
|
|
|
namespace VFXReviewWorker;
|
|
|
|
/// <summary>
|
|
/// Renews a job's lease with progress reports while the worker does something
|
|
/// long that reports nothing on its own — the High quality encode and the
|
|
/// uploads can each outlast a 300 s lease, and an expired lease lets the
|
|
/// reaper hand the job to another machine mid-upload.
|
|
///
|
|
/// Each report also replaces the job's log tail with what the worker is doing
|
|
/// now ("Uploading High quality MP4: 12.0 / 34.0 MB…"). Without it the app
|
|
/// keeps showing aerender's last frame line and a busy job looks hung.
|
|
/// </summary>
|
|
internal sealed class LeaseKeepAlive : IAsyncDisposable
|
|
{
|
|
private readonly ApiClient _api;
|
|
private readonly string _jobId;
|
|
private readonly string _machineId;
|
|
private readonly CancellationTokenSource _cts;
|
|
private readonly SemaphoreSlim _wake = new(0);
|
|
private readonly Task _loop;
|
|
private readonly object _lock = new();
|
|
private readonly List<string> _lines = new();
|
|
private string? _current;
|
|
|
|
public LeaseKeepAlive(ApiClient api, string jobId, string machineId, TimeSpan interval, CancellationToken shutdown)
|
|
{
|
|
_api = api;
|
|
_jobId = jobId;
|
|
_machineId = machineId;
|
|
_cts = CancellationTokenSource.CreateLinkedTokenSource(shutdown);
|
|
var token = _cts.Token;
|
|
_loop = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
while (!token.IsCancellationRequested)
|
|
{
|
|
// Best-effort by contract (ApiClient.ProgressAsync never throws
|
|
// on transport errors); progress stays at 1.0 — the render is done.
|
|
await _api.ProgressAsync(_jobId, _machineId, 1.0, null, null, null, Render(), token);
|
|
await _wake.WaitAsync(interval, token);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// stopped
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>Adds a finished step to the status shown in the app.</summary>
|
|
public void Line(string text)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_current = null;
|
|
_lines.Add($"{DateTime.Now:HH:mm:ss} {text}");
|
|
}
|
|
_wake.Release();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Progress callback for <see cref="ApiClient.UploadArtifactAsync"/>: keeps
|
|
/// one live line ("Uploading X: 12.0 / 34.0 MB (35%, 2.1 MB/s)") up to date.
|
|
/// </summary>
|
|
public Action<long> Track(string label, string filePath)
|
|
{
|
|
long total = File.Exists(filePath) ? new FileInfo(filePath).Length : 0;
|
|
var sw = Stopwatch.StartNew();
|
|
lock (_lock) _current = $"Uploading {label}: 0.0 / {Mb(total)} MB";
|
|
_wake.Release();
|
|
return sent =>
|
|
{
|
|
var pct = total > 0 ? 100.0 * sent / total : 0;
|
|
var speed = sent / 1048576.0 / Math.Max(0.001, sw.Elapsed.TotalSeconds);
|
|
lock (_lock) _current = $"Uploading {label}: {Mb(sent)} / {Mb(total)} MB ({pct:F0}%, {speed:F1} MB/s)";
|
|
};
|
|
}
|
|
|
|
private static string Mb(long bytes) => (bytes / 1048576.0).ToString("F1");
|
|
|
|
private string Render()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var sb = new StringBuilder();
|
|
foreach (var l in _lines) sb.AppendLine(l);
|
|
if (_current is not null) sb.AppendLine($"{DateTime.Now:HH:mm:ss} {_current}");
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
_cts.Cancel();
|
|
try { await _loop; } catch { /* already reported */ }
|
|
_cts.Dispose();
|
|
_wake.Dispose();
|
|
}
|
|
}
|