Costruire una libreria di esecuzione effimera riutilizzabile (Italiano (Italian))

Costruire una libreria di esecuzione effimera riutilizzabile

Friday, 12 December 2025

//

15 minute read

Dentro **Parte 1: Fuoco e non Abbastanza Dimentica**Abbiamo esplorato la teoria dietro l'esecuzione effimera - flussi di lavoro asincroni limitati, privati, debuggabili che ricordano quanto basta per essere utili e poi evaporare.

Questo articolo trasforma quel modello in una libreria riutilizzabile che si può cadere in qualsiasi progetto .NET.

NUGET!!!

Questo è ora nel pacchetto per lo piùlucid.efemerals Nuget anche più di 20 modelli per lo piùlucid.efemerals e 'atomi'.

NuGetCity name (optional, probably does not need a translation) Licenza

File sorgente

La libreria è suddivisa in file ben fattorizzati:

File Scopo
Opzioni effimere.cs Configurazione (valuta, dimensione della finestra, durata, segnali)
Operazione effimera.cs Tracciamento interno del funzionamento con supporto del segnale
Istantanee.cs Record snapshot immutabili esposti ai consumatori
Segnali.cs Eventi di segnale, propagazione, vincoli e il lavello di segnale globale
Generatore Effimero. Fast XxHash64-based ID generation
ConcurrencyGates.cs Limitazione della concorrenza fissa e regolabile
StringPatternMatcher.cs Modello in stile Glob per il filtraggio del segnale
ParallelEfemeral.cs Metodi statici di estensione (EphemeralForEachAsync)
Coordinatore EphemeralWork.cs Coordinatore della coda di lavoro a lunga durata
EfemeralKeyedWorkCoordinator.cs Esecuzione sequenziale per chiave con programmazione equa
EfemeralResultCoordinator.cs Variante del coordinatore della cattura dei risultati
SignalDispatcher.cs Instradamento del segnale asincrono con motivo corrispondente
DipendenzaIniezione.cs Metodi di estensione DI e implementazioni di fabbrica
Esempi/SignalingHttpClient.cs Emissione del segnale a grana fine del campione per le chiamate HTTP

E prove complete coprendo tutti i casi di orlo.


Prima e dopo

Ecco cosa stiamo sostituendo:

// ❌ Before: Fire-and-forget black hole
_ = Task.Run(() => ProcessAsync(item));
// No visibility. No debugging. No idea if it worked.

// ❌ Or: Blocking everything
await ProcessAsync(item);  // Hope you like waiting...

E quello che stiamo costruendo:

// ✅ After: Trackable, bounded, debuggable
await coordinator.EnqueueAsync(item);

// Instant visibility
Console.WriteLine($"Pending: {coordinator.PendingCount}");
Console.WriteLine($"Active: {coordinator.ActiveCount}");
Console.WriteLine($"Failed: {coordinator.TotalFailed}");

// Full operation history
var snapshot = coordinator.GetSnapshot();
var failures = coordinator.GetFailed();

Stessa esecuzione asincrona. Completa osservabilità. Nessun dato utente conservato.


Avvio rapido

Il modello più comune: registrare un coordinatore in DI e iniettarlo:

// Program.cs
services.AddEphemeralWorkCoordinator<TranslationRequest>(
    async (request, ct) => await TranslateAsync(request, ct),
    new EphemeralOptions { MaxConcurrency = 8 });

// Your service
public class TranslationService(EphemeralWorkCoordinator<TranslationRequest> coordinator)
{
    public async Task TranslateAsync(TranslationRequest request)
    {
        await coordinator.EnqueueAsync(request);
        // Returns immediately - work happens in background
    }

    public object GetStatus() => new
    {
        pending = coordinator.PendingCount,
        active = coordinator.ActiveCount,
        completed = coordinator.TotalCompleted,
        failed = coordinator.TotalFailed
    };
}

Di quale variante ho bisogno?

┌─────────────────────────────────────────────────────────────────┐
│                    DECISION TREE                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Processing a collection once?                                  │
│  └─► EphemeralForEachAsync<T> (ParallelEphemeral.cs)            │
│                                                                 │
│  Need a long-lived queue that accepts items over time?          │
│  └─► EphemeralWorkCoordinator<T>                                │
│                                                                 │
│  Need per-entity ordering (user commands, tenant jobs)?         │
│  └─► EphemeralKeyedWorkCoordinator<TKey, T>                     │
│                                                                 │
│  Need to capture results (fingerprints, summaries)?             │
│  └─► EphemeralResultCoordinator<TInput, TResult>                │
│                                                                 │
│  Need multiple coordinators with different configs?             │
│  └─► IEphemeralCoordinatorFactory<T> (like IHttpClientFactory)  │
│                                                                 │
│  Need dynamic concurrency adjustment at runtime?                │
│  └─► Set EnableDynamicConcurrency = true, call SetMaxConcurrency│
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

L'oggetto di configurazione

Da Opzioni effimere.cs:

public sealed class EphemeralOptions
{
    // Concurrency control
    public int MaxConcurrency { get; init; } = Environment.ProcessorCount;
    public int MaxConcurrencyPerKey { get; init; } = 1;
    public bool EnableDynamicConcurrency { get; init; } = false;

    // Window management
    public int MaxTrackedOperations { get; init; } = 200;
    public TimeSpan? MaxOperationLifetime { get; init; } = TimeSpan.FromMinutes(5);

    // Fair scheduling (keyed coordinator)
    public bool EnableFairScheduling { get; init; } = false;
    public int FairSchedulingThreshold { get; init; } = 10;

    // Signal-reactive processing
    public IReadOnlySet<string>? CancelOnSignals { get; init; }
    public IReadOnlySet<string>? DeferOnSignals { get; init; }
    public int MaxDeferAttempts { get; init; } = 10;
    public TimeSpan DeferCheckInterval { get; init; } = TimeSpan.FromMilliseconds(100);

    // Signal infrastructure
    public SignalSink? Signals { get; init; }
    public SignalConstraints? SignalConstraints { get; init; }
    public Action<SignalEvent>? OnSignal { get; init; }

    // Async signal handling
    public Func<SignalEvent, CancellationToken, Task>? OnSignalAsync { get; init; }
    public int MaxConcurrentSignalHandlers { get; init; } = 4;
    public int MaxQueuedSignals { get; init; } = 1000;

    // Observability
    public Action<IReadOnlyCollection<EphemeralOperationSnapshot>>? OnSample { get; init; }
}

Decisioni chiave in materia di progettazione

  • MaxConcurrency Predefiniti per il conteggio della CPU - ragionevole per il lavoro legato alla CPU. Per il lavoro legato all'I/O, aumentarlo.
  • Abilita valuta dinamica consente la regolazione del runtime tramite SetMaxConcurrency() - usa un cancello personalizzato invece di SemaphoreSlim.
  • AnnullaOnSignals/DeferOnSignals rendono i coordinatori reattivi al segnale - rispondono allo stato del sistema ambientale (modelli di supporto) */?/comma lists).
  • OnSignal è sincrono; per uso fan-out asincrono SignalDispatcher oppure AsyncSignalProcessor All'interno del responsabile.
  • SignalConstraints previene gli infiniti cicli di segnalazione con rilevamento del ciclo e limiti di profondità.

Le registrazioni di snapshot

Da Istantanee.cs:

public sealed record EphemeralOperationSnapshot(
    long Id,
    DateTimeOffset Started,
    DateTimeOffset? Completed,
    string? Key,
    bool IsFaulted,
    Exception? Error,
    TimeSpan? Duration,
    IReadOnlyList<string>? Signals = null,
    bool IsPinned = false)
{
    public bool HasSignal(string signal) => Signals?.Contains(signal) == true;
}

// For result-capturing coordinators
public sealed record EphemeralOperationSnapshot<TResult>(
    long Id,
    DateTimeOffset Started,
    DateTimeOffset? Completed,
    string? Key,
    bool IsFaulted,
    Exception? Error,
    TimeSpan? Duration,
    TResult? Result,
    bool HasResult,
    IReadOnlyList<string>? Signals = null,
    bool IsPinned = false);

Questo e' Solo metadati. Notate cosa è non qui:

  • Nessun carico utile
  • Nessun dato di input
  • Nessun contenuto utente

Abbastanza da rispondere "che cosa è successo, quando, e ha funzionato?" - niente di più.


Come questo si confronta con altri approcci

.NET ti dà diversi modi per fare lavori paralleli. Ecco come la libreria Effimera confronta:

Parallelo.ForEachAsync (.NET 6+)

await Parallel.ForEachAsync(items,
    new ParallelOptions { MaxDegreeOfParallelism = 4 },
    async (item, ct) => await ProcessAsync(item, ct));

Meglio per: Semplice elaborazione parallela di collezioni dove non c'è bisogno di visibilità.

Cio' che gli manca:

  • Nessuna operazione di monitoraggio
  • Nessuna esecuzione sequenziale per chiave
  • Nessuna visibilità su ciò che sta funzionando

Usare Effimero quando: È necessario debug / osservabilità, per-key ordering, o l'elaborazione del segnale-reattivo.

Flusso di dati TPL

var block = new ActionBlock<T>(
    async item => await ProcessAsync(item),
    new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 4 });

foreach (var item in items)
    block.Post(item);

block.Complete();
await block.Completion;

Meglio per: Tubi di flusso dati complessi con ramificazione, fusione, batching.

Quello che fa bene:

  • Ricca composizione del gasdotto (link blocks together)
  • Batching integrato, trasformazione, trasmissione
  • Capacità limitata con contropressione

Usa TPL Dataflow quando: Hai bisogno di complesse topologie di pipeline (fan-out, fan-in, routing condizionale).

Usare Effimero quando: È necessario il monitoraggio delle operazioni, più semplice API, o la coordinazione segnale-reattiva.

System.Threading.Channels

var channel = Channel.CreateBounded<T>(100);

// Producer
foreach (var item in items)
    await channel.Writer.WriteAsync(item);
channel.Writer.Complete();

// Consumer (multiple workers)
var workers = Enumerable.Range(0, 4).Select(async _ =>
{
    await foreach (var item in channel.Reader.ReadAllAsync())
        await ProcessAsync(item);
});
await Task.WhenAll(workers);

Meglio per: Modelli produttore-consumatore dove si controllano entrambi i lati.

Quello che fa bene:

  • Ottima prestazione
  • Back-pressure tramite canali delimitati
  • Separazione dei produttori e dei consumatori

Usa i canali quando: Stai costruendo un'infrastruttura personalizzata e hai bisogno del massimo controllo.

Usare Effimero quando: Si desidera il monitoraggio dell'operazione e osservabilità senza la piastra caldaia.

PollyCity name (optional, probably does not need a translation)

var policy = Policy
    .Handle<HttpRequestException>()
    .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));

await policy.ExecuteAsync(() => ProcessAsync(item));

Meglio per: Politiche di resilienza (reti, interruttori di circuito, timeout) per le singole operazioni.

Usa Polly quando: Hai bisogno di resilienza intorno alle chiamate individuali.

Usare Effimero quando: Hai bisogno di coordinamento in molte operazioni con consapevolezza ambientale.

Combinarli: Utilizzare Polly all'interno del vostro corpo di lavoro effimero per la resilienza per-operazione.

MassTransit / NServiceBus

Meglio per: Messaggi distribuiti tra i servizi con code durevoli.

Usa bus di messaggi quando: Il lavoro deve sopravvivere riavvia processo, abbracciare servizi multipli, o richiedere la consegna garantita.

Usare Effimero quando: Il lavoro è in-processo, non ha bisogno di durata, e si desidera osservare leggero.

Tabella di confronto

Avvicinamento Bounded Tracking Per-Key Signals Self-cleaning Complexity
Parallel.ForEachAsync • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • •
Flusso di dati TPL ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' Elevato ' '
Canali ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
Polly N/A ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' basso ' '
Servizi di fondo ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
MassTransit/NServiceBus ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
Biblioteca effimera • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • •

Effimera per ogni sincrono: la versione con un colpo solo

Da ParallelEfemeral.cs:

// Simple parallel processing with tracking
await items.EphemeralForEachAsync(
    async (item, ct) => await ProcessAsync(item, ct),
    new EphemeralOptions { MaxConcurrency = 8 });

// With keyed execution (per-user sequential)
await commands.EphemeralForEachAsync(
    cmd => cmd.UserId,  // Key selector
    async (cmd, ct) => await ExecuteCommandAsync(cmd, ct),
    new EphemeralOptions
    {
        MaxConcurrency = 32,
        MaxConcurrencyPerKey = 1  // Sequential per user
    });

Perché la materia dei tubi a chiave

Immaginate di elaborare i comandi utente:

  • L'utente A invia comandi 1, 2, 3
  • L'utente B invia i comandi 4, 5, 6

Senza chiave, questi potrebbero eseguire come: 1, 4, 2, 5, 3, 6 - interleaved.

Con MaxConcurrencyPerKey = 1:

  • I comandi dell'utente A eseguono in ordine: 1 → 2 → 3
  • I comandi dell'utente B eseguono in ordine: 4 → 5 → 6
  • Ma A e B possono funzionare in parallelo

Questo e' per entità sequenziale, globalmente parallelo - critico per i sistemi in cui l'ordine ha importanza all'interno di un'entità.


Il coordinatore del lavoro: una coda a lungo termine

Da Coordinatore EphemeralWork.cs:

await using var coordinator = new EphemeralWorkCoordinator<TranslationRequest>(
    async (request, ct) => await TranslateAsync(request, ct),
    new EphemeralOptions
    {
        MaxConcurrency = 8,
        MaxTrackedOperations = 500,
        EnableDynamicConcurrency = true  // Allow runtime adjustment
    });

// Enqueue items over time
await coordinator.EnqueueAsync(new TranslationRequest("Hello", "es"));

// Check status anytime
Console.WriteLine($"Pending: {coordinator.PendingCount}");
Console.WriteLine($"Active: {coordinator.ActiveCount}");

// Get snapshots
var snapshot = coordinator.GetSnapshot();
var running = coordinator.GetRunning();
var failed = coordinator.GetFailed();
var completed = coordinator.GetCompleted();

// Control flow
coordinator.Pause();   // Stop pulling new work
coordinator.Resume();  // Continue

// Adjust concurrency at runtime (requires EnableDynamicConcurrency)
coordinator.SetMaxConcurrency(16);

// Pin important operations to survive eviction
coordinator.Pin(operationId);
coordinator.Unpin(operationId);
coordinator.Evict(operationId);

// When done
coordinator.Complete();
await coordinator.DrainAsync();

Flussi continui con IAsyncNumerable

await using var coordinator = EphemeralWorkCoordinator<Message>.FromAsyncEnumerable(
    messageStream,  // IAsyncEnumerable<Message>
    async (msg, ct) => await ProcessMessageAsync(msg, ct),
    new EphemeralOptions { MaxConcurrency = 16 });

await coordinator.DrainAsync();

Il coordinatore chiave: per-entity Pipelines

Da EfemeralKeyedWorkCoordinator.cs:

await using var coordinator = new EphemeralKeyedWorkCoordinator<string, Command>(
    cmd => cmd.UserId,  // Key selector
    async (cmd, ct) => await ExecuteCommandAsync(cmd, ct),
    new EphemeralOptions
    {
        MaxConcurrency = 32,
        MaxConcurrencyPerKey = 1,      // Per-user sequential
        EnableFairScheduling = true,   // Prevent hot user starvation
        FairSchedulingThreshold = 10   // Reject if user has 10+ pending
    });

// TryEnqueue returns false if fair scheduling rejects
if (!coordinator.TryEnqueue(hotUserCommand))
{
    await DeferCommandAsync(hotUserCommand);
}

// Per-key visibility
var pendingForUser = coordinator.GetPendingCountForKey("user-123");
var opsForUser = coordinator.GetSnapshotForKey("user-123");

Coordinatori per la valutazione dei risultati

Da EfemeralResultCoordinator.cs:

await using var coordinator = new EphemeralResultCoordinator<SessionInput, SessionResult>(
    async (input, ct) =>
    {
        var fingerprint = await ComputeFingerprintAsync(input.Events, ct);
        return new SessionResult(fingerprint, input.Events.Length);
    },
    new EphemeralOptions { MaxConcurrency = 16 });

await coordinator.EnqueueAsync(session);
coordinator.Complete();
await coordinator.DrainAsync();

// Get just the results (no metadata)
var results = coordinator.GetResults();

// Get snapshots with results + metadata
var snapshots = coordinator.GetSnapshot();

// Get base snapshots without results (privacy-safe)
var baseSnapshots = coordinator.GetBaseSnapshot();

// Filter by success/failure
var successful = coordinator.GetSuccessful();
var failed = coordinator.GetFailed();

Concurrency Control (Concurrency Control)

Da ConcurrencyGates.cs:

La libreria fornisce due meccanismi di controllo delle concorrenze:

FixedConcurrencyGate (Predefinito)

  • Sostenuto da SemaphoreSlim
  • Ottima prestazione del percorso a caldo
  • Impossibile regolare al runtime

RegolabileConcurrencyGateCity name (optional, probably does not need a translation)

  • Implementazione personalizzata con Queue<WaiterEntry>
  • Supporti UpdateLimit() al runtime
  • Abilitato tramite EnableDynamicConcurrency = true
// Dynamic concurrency adjustment
var coordinator = new EphemeralWorkCoordinator<T>(body,
    new EphemeralOptions
    {
        MaxConcurrency = 4,
        EnableDynamicConcurrency = true
    });

// Later, based on system load:
coordinator.SetMaxConcurrency(16);  // Scale up
coordinator.SetMaxConcurrency(2);   // Scale down

Il modello di fabbrica: Coordinatori nominati

Da DipendenzaIniezione.cs:

Come IHttpClientFactory, puoi registrare configurazioni con nome:

// Registration
services.AddEphemeralWorkCoordinator<TranslationRequest>("fast",
    async (request, ct) => await FastTranslateAsync(request, ct),
    new EphemeralOptions { MaxConcurrency = 32 });

services.AddEphemeralWorkCoordinator<TranslationRequest>("accurate",
    async (request, ct) => await AccurateTranslateAsync(request, ct),
    new EphemeralOptions { MaxConcurrency = 4 });

// Usage
public class TranslationService(IEphemeralCoordinatorFactory<TranslationRequest> factory)
{
    private readonly EphemeralWorkCoordinator<TranslationRequest> _fast =
        factory.CreateCoordinator("fast");
    private readonly EphemeralWorkCoordinator<TranslationRequest> _accurate =
        factory.CreateCoordinator("accurate");
}

Garanzie di fabbrica

  1. Stesso nome = stessa istanza - Sto chiamando. CreateCoordinator("fast") due volte ritorna lo stesso coordinatore
  2. Nomi diversi = istanze diverse - "fast" e "accurate" ottenere coordinatori separati
  3. Creazione pigra - I coordinatori vengono creati solo quando prima richiesto
  4. Convalida di configurazione - Richiedere un nome non registrato crea un utile errore

API di interrogazione del segnale

Tutti i coordinatori forniscono metodi ottimizzati di interrogazione dei segnali:

// Get all signals
var signals = coordinator.GetSignals();

// Filter by key (zero-allocation)
var userSignals = coordinator.GetSignalsByKey("user-123");

// Filter by time range
var recentSignals = coordinator.GetSignalsSince(DateTimeOffset.UtcNow.AddMinutes(-5));
var rangeSignals = coordinator.GetSignalsByTimeRange(from, to);

// Filter by signal name or pattern
var rateSignals = coordinator.GetSignalsByName("rate-limit");
var httpSignals = coordinator.GetSignalsByPattern("http.*");

// Check existence (short-circuits on first match)
if (coordinator.HasSignal("rate-limit"))
    await ThrottleAsync();

if (coordinator.HasSignalMatching("error.*"))
    await AlertAsync();

// Count signals efficiently (no allocation)
var totalSignals = coordinator.CountSignals();
var errorCount = coordinator.CountSignals("error");
var httpCount = coordinator.CountSignalsMatching("http.*");

Ottimizzazioni di produzione

Generazione ID veloce

Da Generatore Effimero.:

internal static class EphemeralIdGenerator
{
    private static long _counter;
    private static readonly long _processStart = Environment.TickCount64;
    private static readonly int _processId = Environment.ProcessId;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static long NextId()
    {
        var counter = Interlocked.Increment(ref _counter);

        // Combine counter with process-unique seed
        Span<byte> buffer = stackalloc byte[24];
        BitConverter.TryWriteBytes(buffer, _processStart);
        BitConverter.TryWriteBytes(buffer.Slice(8), _processId);
        BitConverter.TryWriteBytes(buffer.Slice(16), counter);

        return unchecked((long)XxHash64.HashToUInt64(buffer));
    }
}
  • Senza assegnazione (impieghi stackalloc)
  • Filettatura sicura (impieghi Interlocked.Increment)
  • Unica per tutti i processi (include l'ID del processo)
  • Non consequenziale (Hash diffonde il contatore)

Memory-Safe Long Lived Operation

I coordinatori non memorizzano Task riferimenti - solo contatori:

private int _activeTaskCount;
private readonly TaskCompletionSource _drainTcs;

// In ExecuteItemAsync:
finally
{
    // Signal drain when last task completes AND channel iteration is done
    if (Interlocked.Decrement(ref _activeTaskCount) == 0 &&
        Volatile.Read(ref _channelIterationComplete))
    {
        _drainTcs.TrySetResult();
    }
}

Pulizia della serratura per chiave

Il coordinatore chiave pulisce automaticamente i semafori inattivi per chiave:

private sealed class KeyLock(SemaphoreSlim gate, int maxCount)
{
    public SemaphoreSlim Gate { get; } = gate;
    public int MaxCount { get; } = maxCount;
    public long LastUsedTicks = Environment.TickCount64;
}

// Cleanup runs periodically, removes locks idle > 60 seconds

Esempio completo

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Named coordinators
builder.Services.AddEphemeralWorkCoordinator<TranslationRequest>("fast",
    async (req, ct) => await FastTranslateAsync(req, ct),
    new EphemeralOptions { MaxConcurrency = 16 });

// Keyed coordinator for per-user commands
builder.Services.AddEphemeralKeyedWorkCoordinator<string, UserCommand>("commands",
    cmd => cmd.UserId,
    sp =>
    {
        var handler = sp.GetRequiredService<ICommandHandler>();
        return async (cmd, ct) => await handler.HandleAsync(cmd, ct);
    },
    new EphemeralOptions
    {
        MaxConcurrency = 32,
        MaxConcurrencyPerKey = 1,
        EnableFairScheduling = true,
        CancelOnSignals = new HashSet<string> { "system-overload" }
    });

var app = builder.Build();
// Controller
[ApiController]
[Route("api")]
public class WorkController : ControllerBase
{
    private readonly EphemeralWorkCoordinator<TranslationRequest> _translator;
    private readonly EphemeralKeyedWorkCoordinator<string, UserCommand> _commands;

    public WorkController(
        IEphemeralCoordinatorFactory<TranslationRequest> translationFactory,
        IEphemeralKeyedCoordinatorFactory<string, UserCommand> commandFactory)
    {
        _translator = translationFactory.CreateCoordinator("fast");
        _commands = commandFactory.CreateCoordinator("commands");
    }

    [HttpPost("translate")]
    public async Task<IActionResult> Translate([FromBody] TranslationRequest request)
    {
        await _translator.EnqueueAsync(request);
        return Ok(new { pending = _translator.PendingCount });
    }

    [HttpPost("command")]
    public IActionResult SubmitCommand([FromBody] UserCommand command)
    {
        if (!_commands.TryEnqueue(command))
            return StatusCode(429, "Too many pending commands for this user");
        return Ok();
    }

    [HttpGet("status")]
    public IActionResult GetStatus() => Ok(new
    {
        translator = new
        {
            pending = _translator.PendingCount,
            active = _translator.ActiveCount,
            completed = _translator.TotalCompleted,
            failed = _translator.TotalFailed,
            hasRateLimit = _translator.HasSignal("rate-limit")
        },
        commands = new
        {
            pending = _commands.PendingCount,
            active = _commands.ActiveCount,
            errorCount = _commands.CountSignalsMatching("error.*")
        }
    });
}

Conclusione

Abbiamo costruito una libreria di esecuzione effimera completa con:

  1. EphemeralForEachAsync - Lavorazione parallela monofoto con tracciamento
  2. EphemeralWorkCoordinator - Code osservabili a lunga durata
  3. EphemeralKeyedWorkCoordinator - Esecuzione sequenziale per entità con programmazione equa
  4. EphemeralResultCoordinator - Variante di cattura dei risultati
  5. Modello di fabbrica - Configurazioni con nome come IHttpClientFactory
  6. Concorrenze dinamiche - Regolazione del runtime del parallelismo
  7. Infrastrutture di segnalazione - Emissione del segnale integrato e interrogatorio

Il modello si trova in un punto dolce:

  • Più osservabile di Parallel.ForEachAsync
  • Più semplice del flusso di dati TPL
  • Più integrato dei canali grezzi
  • Privacy-safe by design

Fuoco... e non dimenticarti.


Collegamenti

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.