Editor/HumanoidHandRetargeter/HandRetargetApi.Jobs.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace HumanoidHandRetargeter.Editor;
public static partial class HandRetargetApi
{
static readonly object Gate=new();
static readonly SemaphoreSlim Queue=new(1,1);
static readonly Dictionary<string,Job> Jobs=new();
sealed class Job
{
public string Id=Guid.NewGuid().ToString("N"),Status="queued",Stage="queued";
public DateTime Created=DateTime.UtcNow;
public object? Result,Error;
public CancellationTokenSource Cancellation=new();
public bool Finished=>Status is "succeeded" or "failed" or "cancelled";
public object Snapshot()=>new{version=1,jobId=Id,status=Status,stage=Stage,createdUtc=Created,result=Result,error=Error,cancellationRequested=Cancellation.IsCancellationRequested};
}
public static string Submit(string requestJson)
{
Request request;
try{request=Parse(requestJson);}
catch(Exception ex) when(ex is JsonException or ArgumentException){return Serialize(new{version=1,error=new{code="invalid_request",message=ex.Message}});}
Job job;
lock(Gate)
{
if(Jobs.Values.Count(j=>!j.Finished)>=8)return Serialize(new{version=1,error=new{code="queue_full",message="At most eight jobs may be pending."}});
foreach(var old in Jobs.Values.Where(j=>j.Finished).OrderBy(j=>j.Created).Take(Math.Max(0,Jobs.Count-63)).ToArray())
{Jobs.Remove(old.Id);old.Cancellation.Dispose();}
job=new();Jobs.Add(job.Id,job);
}
_=Process(job,request);
lock(Gate)return Serialize(job.Snapshot());
}
public static string GetJob(string jobId)
{
lock(Gate)return Jobs.TryGetValue(jobId??"",out var job)?Serialize(job.Snapshot()):Unknown();
}
public static string ListJobs(){lock(Gate)return Serialize(new{version=1,jobs=Jobs.Values.Select(j=>j.Snapshot()).ToArray()});}
public static string Cancel(string jobId)
{
lock(Gate)
{
if(!Jobs.TryGetValue(jobId??"",out var job))return Unknown();
if(!job.Finished)job.Cancellation.Cancel();
return Serialize(job.Snapshot());
}
}
static string Unknown()=>Serialize(new{version=1,error=new{code="job_not_found",message="Unknown job ID, or jobs were cleared by editor reload."}});
static async Task Process(Job job,Request request)
{
await Task.Yield();var entered=false;
try
{
await Queue.WaitAsync(job.Cancellation.Token);entered=true;
lock(Gate)job.Status="running";
var result=await Run(request,job.Cancellation.Token,stage=>{lock(Gate)job.Stage=stage;});
// If export committed before cancellation arrived, report its successful result.
lock(Gate){job.Result=result;job.Status="succeeded";job.Stage="complete";}
}
catch(OperationCanceledException){lock(Gate){job.Status="cancelled";job.Stage="cancelled";}}
catch(Exception ex)
{
lock(Gate)
{
job.Error=new{code=ex is MappingRequiredException?"mapping_required":ex is ArgumentException?"invalid_request":"operation_failed",
message=ex.Message,details=(ex as MappingRequiredException)?.Details};
job.Status="failed";
}
}
finally{if(entered)Queue.Release();}
}
}