using OpenNest.Data; using OpenNest.Engine.Jobs; using OpenNest.Engine.Jobs.Remote; namespace OpenNest.Server.Worker; /// /// Collects a running job's engine progress and stores it on the nest server at most once per /// interval: only the latest stage and the latest plan are sent, so a chatty engine costs one /// small write and one plan write per interval at most. Each round, a report (or, with nothing /// new to send, a read of the job) also tells the worker promptly that someone asked to stop. /// public sealed class NestJobReporter : IProgress { private readonly object _sync = new(); private RemoteNestProgress? _stage; private RemoteNestPlan? _plan; public void Report(NestJobProgress value) { if (value is null) return; // Copied here, on the engine's thread, so later changes to its objects cannot leak in; // serialized later, off that thread. if (value.Stage == NestJobStage.PlanImproved) { if (value.Plan is null) return; var plan = RemoteNestPlan.From(value.Plan); lock (_sync) _plan = plan; return; } var stage = RemoteNestProgress.From(value); lock (_sync) _stage = stage; } /// /// Sends reports every until is /// cancelled. Returns why the job must stop (a cancel request, or the worker lost it), /// or when cancelled first. /// public async Task RunAsync(RemoteNestJobs jobs, Guid id, string workerId, TimeSpan interval, ILogger logger, CancellationToken token) { while (true) { try { await Task.Delay(interval, token); } catch (OperationCanceledException) { return NestJobWorker.StopReason.None; } RemoteNestProgress? stage; RemoteNestPlan? plan; lock (_sync) { (stage, plan) = (_stage, _plan); (_stage, _plan) = (null, null); } try { var beats = new List(); if (plan is not null) beats.Add(await jobs.ReportPlanAsync(id, workerId, plan.ToJson(), token)); if (stage is not null) beats.Add(await jobs.ReportProgressAsync(id, workerId, stage.ToJson(), token)); if (beats.Count == 0) { // Nothing new: read the job instead, which costs the server no write. var job = await jobs.GetAsync(id, token); beats.Add(job is { State: NestJobState.Running } ? new NestJobHeartbeat { CancelRequested = job.CancelRequested } : null); } if (beats.Any(b => b is null)) return NestJobWorker.StopReason.LeaseLost; if (beats.Any(b => b!.CancelRequested)) return NestJobWorker.StopReason.CancelRequested; } catch (OperationCanceledException) when (token.IsCancellationRequested) { return NestJobWorker.StopReason.None; } catch (Exception ex) when (ex is IOException or HttpRequestException or TaskCanceledException) { // Keep solving; the next round sends whatever is latest by then. lock (_sync) { _stage ??= stage; _plan ??= plan; } logger.LogWarning("Progress for nest job {Id} not stored: {Message}", id, ex.Message); } } } }