09eaf360a3
SharedKernel: IWorkerModule seam (RegisterWorker runs in the worker host only).
Bootstrap: AddTeamUpWorkerServices; the worker host now wires it.
Assembler module (schema "assembler", InitialAssembler migration):
- Job (Pending→Processing→Done/Failed) + AgentRun (Queued→Running→Completed/Failed) entities.
- JobQueue: enqueue + ClaimNextAsync using `FOR UPDATE SKIP LOCKED` in a transaction.
- AgentRunExecutor (Increment-1 placeholder — real assemble/model/parse lands in Increment 2).
- JobProcessor BackgroundService drains the queue on the worker host (web off the model path).
- POST /api/assembler/runs enqueues a run; GET /api/assembler/runs/{id} reads it.
Verified: build green; ArchitectureTests 8/8 (Assembler references only SharedKernel);
IntegrationTests 28/28 incl. enqueue→claim(SKIP LOCKED)→process→Completed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
51 lines
1.8 KiB
C#
51 lines
1.8 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using TeamUp.Modules.Assembler.Queue;
|
|
using TeamUp.Modules.Assembler.Runtime;
|
|
|
|
namespace TeamUp.Modules.Assembler.Worker;
|
|
|
|
/// <summary>Drains the agent-run queue on the worker host: claim (SKIP LOCKED) → process, repeat.</summary>
|
|
internal sealed class JobProcessor(IServiceScopeFactory scopeFactory, ILogger<JobProcessor> logger) : BackgroundService
|
|
{
|
|
private static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(2);
|
|
private readonly string _worker = $"{Environment.MachineName}:{Environment.ProcessId}";
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
logger.LogInformation("Agent-run job processor started ({Worker}).", _worker);
|
|
|
|
using var timer = new PeriodicTimer(PollInterval);
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await DrainAsync(stoppingToken);
|
|
await timer.WaitForNextTickAsync(stoppingToken);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task DrainAsync(CancellationToken cancellationToken)
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
await using var scope = scopeFactory.CreateAsyncScope();
|
|
var queue = scope.ServiceProvider.GetRequiredService<JobQueue>();
|
|
var job = await queue.ClaimNextAsync(_worker, cancellationToken);
|
|
if (job is null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var executor = scope.ServiceProvider.GetRequiredService<AgentRunExecutor>();
|
|
await executor.ProcessAsync(job, cancellationToken);
|
|
}
|
|
}
|
|
}
|