using System.Diagnostics; using System.Text; namespace VFXReviewWorker; /// /// 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. /// 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 _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 } }); } /// Adds a finished step to the status shown in the app. public void Line(string text) { lock (_lock) { _current = null; _lines.Add($"{DateTime.Now:HH:mm:ss} {text}"); } _wake.Release(); } /// /// Progress callback for : keeps /// one live line ("Uploading X: 12.0 / 34.0 MB (35%, 2.1 MB/s)") up to date. /// public Action 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(); } }