Fuoco e non sparare Abbastanza Dimentica - esecuzione effimera senza Stato (Italiano (Italian))

Fuoco e non sparare Abbastanza Dimentica - esecuzione effimera senza Stato

Friday, 12 December 2025

//

16 minute read

La maggior parte dei sistemi async o ricordano troppo (log, code, spazzatura persistente che non hai mai voluto)... o non ricordano nulla (buchi neri incendiati e dimenticati che svaniscono nel momento in cui qualcosa va storto).

Questo articolo introduce qualcosa di diverso:

Uno schema in cui ogni operazione asincrona diventa una piccola tracciabile, ispezionabile, ri-recuperabile, Unità di attività autopulente -e l'intero sistema rimane privato, limitato, veloce e deterministico.

E' il compagno del mio precedente articolo, Imparare le LRU - Quando l'eccesso di capacità rende il vostro sistema migliore. Che uno ha esplorato come limitata memoria + scadenza scorrevole diventa un meccanismo di sopravvivenza.

Questo esplora l'altra metà: esecuzione limitata, effimera -come un piccolo rolling buffer di attività attive diventa un debugger, un log di eventi e un motore di riproduzione, senza mantenere alcun dato utente.

L'esempio viene dal widget di traduzione del mio blog - una piccola UI per la traduzione di contenuti markdown on-the-fly. Sembra banale. Sotto il cofano, implementa i modelli è possibile rubare per qualsiasi flusso di lavoro async.

Questo e' Parte 1 di una serie in due parti:

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


Il problema - flussi di lavoro sincronizzati Crea stato nascosto

Gli sviluppatori ASP.NET tendono a scegliere tra tre opzioni sbagliate:

Fuoco e dimentico

Tu Task.Run() Qualcosa, sperare che finisca, e quando esplode non ottieni niente. Nessuna correlazione, nessuna ricostruzione, nessuna idea di cosa abbia fallito.

Fuoco e attesa

Blocchi i fili che non dovresti bloccare, il flusso muore, la latenza esplode, i tuoi utenti ti odiano.

Coda/canali ovunque

Ora hai:

  • code senza limiti
  • stato persistente che non intendevi mantenere
  • PII accidentalmente memorizzato da qualche parte
  • un sistema distribuito in cui si desiderava solo una semplice operazione

Tracciamento distribuito

Utile... ma esterno. Inoltre: non può riprodurre nulla e spesso trapela dati che non hai mai pensato di memorizzare.

Cio' che vogliamo in realta' e':

  • di breve durata
  • privato
  • debuggabile
  • ispezionabile
  • limitato
  • recuperabile se necessario
  • e poi se n'è andato

Una memoria "appena abbastanza lunga" di ciò che il sistema sta facendo - e niente di più.


Il modello - Fuoco e Non proprio. Dimentica

Ecco l'idea in una frase:

Preleva immediatamente un'operazione asincrona, rintracciala esplicitamente tramite un TaskCompletionSource, ripuliscilo deterministicamente e mantieni un piccolo buffer di rotolamento degli ultimi compiti in modo da poterli ispezionare o recuperarli - senza mantenere alcun contenuto utente.

Si trova a metà strada tra:

  • approvvigionamento di eventi
  • tracciatura
  • code di lavoro
  • futures/promesse
  • sessioni effimere

...senza diventare nessuno di loro.

flowchart TB
    subgraph Client["Client Request"]
        R[Start Translation]
    end

    subgraph API["API Layer"]
        A[Create TaskId] --> B[Create TaskCompletionSource]
        B --> C[Queue to Channel]
        C --> D[Return TaskId Immediately]
    end

    subgraph Cache["Ephemeral Cache (Bounded)"]
        E[Store TranslateTask]
        F[Max 5 per user]
        G[6hr absolute / 1hr sliding expiry]
    end

    subgraph Worker["Background Worker"]
        H[Read from Channel]
        H --> I[Execute Translation]
        I --> J[Complete TCS]
    end

    R --> A
    B --> E
    E --> F --> G
    C --> H

    style Cache fill:none,stroke:#10b981,stroke-width:2px
    style Worker fill:none,stroke:#6366f1,stroke-width:2px

Anatomia di un compito effimero

La struttura dei dati di base è un TranslateTask - una piccola unità di esecuzione atomica sostenuta da un Task<TaskCompletion>:

// From TranslateTask.cs
public class TranslateTask(
    string taskId,
    DateTime startTime,
    string language,
    Task<TaskCompletion>? task)
    : TranslateResultTask(taskId, startTime, language)
{
    public Task<TaskCompletion>? Task { get; init; } = task;
}

public record TaskCompletion(
    string? TranslatedMarkdown,
    string OriginalMarkdown,
    string Language,
    bool Complete,
    DateTime? EndTime);

Contiene:

  • Un ID unico (corrispondenti a tutto il flusso)
  • Un timestamp (quando è iniziato)
  • Metadati (lingua -non contenuto utente!)
  • Un riferimento all'attuale Task<TaskCompletion>
  • Durata (computata all'atto dell'accesso)
  • Stato errore (derivato dallo stato attività)
  • Risultato finale (solo se completato con successo)

Niente di persistente, niente di scritto su disco, niente di memorizzato al di là di un rolling window.

AWS ha funzioni passo. Azure ha funzioni durabili. Hai... 30 linee di codice che non richiedono una nuvola.


Il modello di risorsa del completamento del task -Bridging Fire-and-Forget to Fire-and-Track

La magia accade nel BackgroundTranslateService. Invece di fuoco-e-dimenticare, usiamo un TaskCuplementFonte -un costrutto simile a una promessa che ci permette di tornare immediatamente mentre il lavoro avviene in background.

// From BackgroundTranslateService.cs
private readonly Channel<(PageTranslationModel, TaskCompletionSource<TaskCompletion>)>
    _translations = Channel.CreateUnbounded<(PageTranslationModel, TaskCompletionSource<TaskCompletion>)>();

private async Task<Task<TaskCompletion>> Translate(PageTranslationModel message)
{
    // Create a TaskCompletionSource that will eventually hold the result
    var tcs = new TaskCompletionSource<TaskCompletion>();

    // Send the translation request along with the TCS to be processed
    await _translations.Writer.WriteAsync((message, tcs));

    // Return the Task immediately -caller can await it or check status later
    return tcs.Task;
}

Cos'è un TaskCompletionFonte?

Se non l'hai usato TaskCompletionSource<T> prima, pensatelo come un prometti di controllare manualmente. A differenza di un regolare Task che completa quando il suo lavoro termina, un TCS completa quando tu chiamata SetResult(), SetException(), oppure SetCanceled().

Questo ti permette di:

  1. Restituisce a Task al chiamante immediatamente
  2. Fare il lavoro reale da qualche altra parte (filo diverso, servizio di sfondo, ecc.)
  3. Completa l'attività quando tu decidere che è fatto

E' il ponte tra "fuoco-e-dimenticato" e "fuoco-e-traccia."


Il livello API -Iniziare una traduzione

Quando un utente fa clic su "Tradurre," il frontend chiama l'API:

// From translations.js
fetch('/api/translate/start-translation', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        Language: shortCode,
        OriginalMarkdown: markdown
    })
})
.then(response => response.json())
.then(taskId => {
    // Got a taskId immediately -translation is running in background
    console.log("Task ID:", taskId);

    // Poll for updates via HTMX
    htmx.ajax('get', "/editor/get-translations", {
        target: '#translations',
        swap: 'innerHTML'
    });
});

L'API restituisce immediatamente con solo un ID attività:

// From TranslateAPI.cs
[HttpPost("start-translation")]
public async Task<Results<Ok<string>, BadRequest<string>>> StartTranslation(
    [FromBody] MarkdownTranslationModel model)
{
    if (!backgroundTranslateService.TranslationServiceUp)
        return TypedResults.BadRequest("Translation service is down");

    // Create a unique identifier for this translation task
    var taskId = Guid.NewGuid().ToString("N");
    var userId = Request.GetUserId(Response);

    // Trigger translation -returns Task<TaskCompletion> immediately
    var translationTask = await backgroundTranslateService.Translate(model);

    // Wrap it in our trackable TranslateTask
    var translateTask = new TranslateTask(taskId, DateTime.Now, model.Language, translationTask);

    // Store in the ephemeral cache (bounded, self-cleaning)
    translateCacheService.AddTask(userId, translateTask);

    // Return the task ID to the client -they can poll for status
    return TypedResults.Ok(taskId);
}

La risposta è immediata. La traduzione effettiva viene eseguita in background. L'utente può interrogare o semplicemente guardare l'aggiornamento dell'interfaccia utente.


La finestra di funzionamento del rotolamento - un buffer autopulente

L'intuizione chiave dal Articolo LRU si applica anche in questo caso:

Mantieni abbastanza storia da debug e ragionare sul sistema... e lasciare che il decadimento naturale cancelli tutto il resto.

// From TranslateCacheService.cs
public class TranslateCacheService(IMemoryCache memoryCache)
{
    public void AddTask(string userId, TranslateTask task)
    {
        if (memoryCache.TryGetValue(userId, out CachedTasks? tasks))
        {
            var currentTasks = tasks?.Tasks ?? new List<TranslateTask>();
            currentTasks = currentTasks.OrderByDescending(x => x.StartTime).ToList();

            // Keep only the 5 most recent tasks -bounded window
            if (currentTasks.Count >= 5)
            {
                var lastTask = currentTasks.Last();
                currentTasks.Remove(lastTask);
            }

            currentTasks.Add(task);
            currentTasks = currentTasks.OrderByDescending(x => x.StartTime).ToList();
            tasks!.Tasks = currentTasks;

            memoryCache.Set(userId, tasks, new MemoryCacheEntryOptions
            {
                AbsoluteExpiration = tasks.AbsoluteExpiration,
                SlidingExpiration = TimeSpan.FromHours(1)
            });
        }
        else
        {
            // First task for this user
            var cachedTasks = new CachedTasks
            {
                Tasks = new List<TranslateTask> { task },
                AbsoluteExpiration = DateTime.Now.AddHours(6)
            };
            memoryCache.Set(userId, cachedTasks, new MemoryCacheEntryOptions
            {
                AbsoluteExpiration = cachedTasks.AbsoluteExpiration,
                SlidingExpiration = TimeSpan.FromHours(1)
            });
        }
    }

    public List<TranslateTask> GetTasks(string userId)
    {
        if (memoryCache.TryGetValue(userId, out CachedTasks? tasks))
            return tasks?.Tasks ?? new List<TranslateTask>();
        return new List<TranslateTask>();
    }

    private class CachedTasks
    {
        public List<TranslateTask> Tasks { get; set; } = new();
        public DateTime AbsoluteExpiration { get; set; }
    }
}

Ogni nuovo compito:

  • Viene aggiunto al rolling window dell'utente
  • Spinge fuori il più vecchio se siamo alla capacità (5 max)
  • Evapori completamente dopo 6 ore (assoluto) o 1 ora di inattività (scivolo)
flowchart LR
    subgraph Window["Per-User Task Window (Max 5)"]
        T1[Oldest Task] --- T2[Older] --- T3[Recent] --- T4[Newer] --- T5[Newest]
    end

    New[New Task] -->|Enqueue| Window
    T1 -->|Evicted| Gone[(Expired)]

    style Window fill:none,stroke:#10b981,stroke-width:2px
    style Gone fill:none,stroke:#ef4444,stroke-width:2px

Nessun rischio di ritenzione. Nessun PII nella cache (solo ID attività, timestamp e codici linguistici). Nessun mal di testa GDPR.


Concorrenza legata - Il governatore Loop

Il servizio di background implementa un Concorrenze fisse loop utilizzando un lettore di canali e Task.WhenAny:

// From BackgroundTranslateService.cs
private async Task TranslateFilesAsync(CancellationToken cancellationToken)
{
    var processingTasks = new List<Task>();

    while (!cancellationToken.IsCancellationRequested)
    {
        // Fill up to IPCount concurrent tasks (e.g., 4 parallel translations)
        while (processingTasks.Count < markdownTranslatorService.IPCount &&
               !cancellationToken.IsCancellationRequested)
        {
            var item = await _translations.Reader.ReadAsync(cancellationToken);
            var translateModel = item.Item1;
            var tcs = item.Item2;

            // Start the task and add it to the list
            var task = TranslateTask(cancellationToken, translateModel, item, tcs);
            processingTasks.Add(task);
        }

        // Wait for ANY of the tasks to complete
        var completedTask = await Task.WhenAny(processingTasks);
        processingTasks.Remove(completedTask);

        // Handle exceptions if needed
        try
        {
            await completedTask;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error translating markdown");
        }
    }
}

E' come se regolatore su un motore a vapore: più carico → contropressione costruisce → strozzatura naturale → stabilità.

Il modello ti dà:

  • Nessuna spirale di sovraccarico - non si possono deporre compiti illimitati
  • Comportamento morbido in tempo reale - latenza legata
  • Levigatura naturale delle esplosioni -canale buffers i picchi
  • Stabilità sotto carico -degrada con grazia

Completare laFonte del Completamento del TaskCource

Quando il lavoro di traduzione effettiva finisce, completiamo il TCS:

// From BackgroundTranslateService.cs
private async Task TranslateTask(
    CancellationToken cancellationToken,
    PageTranslationModel translateModel,
    (PageTranslationModel, TaskCompletionSource<TaskCompletion>) item,
    TaskCompletionSource<TaskCompletion> tcs)
{
    try
    {
        await retryPolicy.ExecuteAsync(async () =>
        {
            // Do the actual translation work
            var translatedMarkdown = await markdownTranslatorService.TranslateMarkdown(
                translateModel.OriginalMarkdown,
                translateModel.Language,
                cancellationToken);

            // SUCCESS: Complete the TCS with the result
            tcs.SetResult(new TaskCompletion(
                translatedMarkdown,
                translateModel.OriginalMarkdown,
                translateModel.Language,
                true,
                DateTime.Now));
        });
    }
    catch (TranslateException e)
    {
        // FAILURE: Complete the TCS with an exception
        tcs.SetException(new Exception($"Translation failed after 3 retries: {e.Message}"));
    }
    catch (Exception e)
    {
        // UNEXPECTED: Complete the TCS with the exception
        tcs.SetException(e);
    }
}

Il chiamante e' Task<TaskCompletion> - che hanno ricevuto subito quando hanno chiamato Translate() - Ora completano.

  • await se vogliono bloccare
  • Controlla IsCompleted Sondaggio
  • Controlla IsFaulted per vedere se è fallito
  • Controlla Result per ottenere il contenuto tradotto

La proiezione dello stato - Deriving State da Task State

Quando mostriamo le attività all'utente, proiettiamo lo stato delle attività live in un modello di visualizzazione:

// From TranslateTask.cs
public TranslateResultTask(TranslateTask task, bool includeMarkdown = false)
{
    TaskId = task.TaskId;
    StartTime = task.StartTime;
    Language = task.Language;

    // Check for faulted state first -a faulted task is also "completed" in .NET terms
    if (task.Task?.IsFaulted == true)
    {
        Failed = true;
        Completed = false;
        TotalMilliseconds = (int)(DateTime.Now - task.StartTime).TotalMilliseconds;
    }
    else if (task.Task?.IsCompletedSuccessfully == true)
    {
        Completed = true;
        Failed = false;
        var endTime = task.Task.Result.EndTime;
        TotalMilliseconds = (int)((endTime - task.StartTime)!).Value.TotalMilliseconds;
        EndTime = endTime;
    }
    else
    {
        // Still in progress
        Completed = false;
        Failed = false;
        TotalMilliseconds = (int)(DateTime.Now - task.StartTime).TotalMilliseconds;
    }

    if (Completed && includeMarkdown)
    {
        var result = task.Task?.Result;
        if (result == null) return;
        OriginalMarkdown = result.OriginalMarkdown;
        TranslatedMarkdown = result.TranslatedMarkdown;
    }
}

Una nota sullo Stato dei compiti

.NET's Task ha alcune strane combinazioni di stato:

  • IsCompleted è vero per entrambi Completamento riuscito E compiti difettosi
  • IsCompletedSuccessfully è vero solo per il successo
  • IsFaulted significa che ha lanciato un'eccezione
  • IsCanceled significa che è stato cancellato

Quindi devi controllare. IsFaulted prima del controllo IsCompleted, o tratterai i fallimenti come successi.


Il Polling UI -HTMX

I sondaggi di visualizzazione per gli aggiornamenti utilizzando HTMX hx-trigger:

@* From _GetTranslations.cshtml *@
@{
    var allCompleted = Model.All(x => x.Completed);
    var trigger = allCompleted ? "none" : "every 5s";
}

<div class="translationpoller"
     hx-get="/editor/get-translations"
     hx-swap="outerHTML"
     hx-trigger="@trigger">
    <table class="table">
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @if (item.Completed)
                    {
                        <a href="#" x-on:click.prevent="viewTranslation('@item.TaskId')">View</a>
                    }
                    else if (item.Failed)
                    {
                        <text>Failed</text>
                    }
                    else
                    {
                        <text>Processing</text>
                    }
                </td>
                <td>
                    @if (item.Completed)
                    {
                        <i class='bx bx-check text-green'></i>
                    }
                    else if (item.Failed)
                    {
                        <i class='bx bx-x text-red'></i>
                    }
                    else
                    {
                        <img src="~/img/3-dots-bounce.svg" />
                    }
                </td>
                <td>@item.Language</td>
                <td>@TimeSpan.FromMilliseconds(item.TotalMilliseconds).Humanize()</td>
            </tr>
        }
    </table>
</div>

La parte intelligente: hx-trigger="@trigger" cambiamenti basati sullo stato:

  • Se le attività sono ancora in esecuzione: sondaggio ogni 5 secondi
  • Se tutti i compiti sono eseguiti: fermare il sondaggio ("none")

Questo e' Autoregolazione dei sondaggi - si ferma automaticamente quando non c'è niente da guardare.


Recupero del risultato

Quando l'utente fa clic su "Visualizza," prendiamo la traduzione completata:

// From TranslateAPI.cs
[HttpGet("get-translation/{taskId}")]
public async Task<Results<JsonHttpResult<TranslateResultTask>, BadRequest<string>>> GetTranslation(
    string taskId)
{
    var userId = Request.GetUserId(Response);
    var tasks = translateCacheService.GetTasks(userId);

    var translationTask = tasks.FirstOrDefault(t => t.TaskId == taskId);
    if (translationTask == null)
        return TypedResults.BadRequest("Task not found");

    // Include the markdown content in the response
    var result = new TranslateResultTask(translationTask, includeMarkdown: true);
    return TypedResults.Json(result);
}

Il JavaScript poi popola l'editor:

// From translations.js
export function viewTranslation(taskId) {
    fetch(`/api/translate/get-translation/${taskId}`)
        .then(response => response.json())
        .then(data => {
            // Show the translated content area
            document.getElementById("translatedcontent").classList.remove("hidden");

            // Populate the editors
            var originalMde = window.mostlylucid.simplemde.getinstance('translatedcontentarea');
            originalMde.value(data.originalMarkdown);

            var mde = window.mostlylucid.simplemde.getinstance('markdowneditor');
            mde.value(data.translatedMarkdown);
        });
}

Perché questo è Privacy-Safe

Perché:

  • Nessun contenuto utente è memorizzato nella cache -solo metadati delle attività (ID, timestamp, lingua)
  • Il contenuto esiste solo nel risultato dell'attività -che è in memoria, allegato al Task
  • La finestra è delimitata -max 5 attività per utente, max 6 ore di ritenzione
  • Tutto decade naturalmente -la scadenza scorrevole pulisce gli utenti inattivi
  • Niente tocca il disco -Nessun registro, nessuna coda, nessun database
  • Non è possibile mantenere accidentalmente PII - Non c'è posto dove possa andare.

Questa è l'architettura Vorrei che browser e frontend framework avessero utilizzato per le operazioni di sessione.


Il comportamento emergente

Cosa ottieni senza pensare troppo:

Benefit How
Stabilità Concurrency delimitata via canale + motivo semaforo
Loop di feedback negativo Più carico → contropressione → memoria delimitata → comportamento coerente
Debug di visibilità Le operazioni esatte sono presenti appena il tempo necessario per ispezionare
Ripristino I risultati completati sono accessibili fino alla loro scadenza
Conservazione zero dei dati Privacy e semplicità allineate per una volta
Auto-ottimizzazione dell'esecuzione Le vecchie mansioni si allontanano, quelle rilevanti rimangono

Proprio come la cache LRU ha affilato la memoria comportamentale, la finestra di funzionamento a rotolamento affila la vista di esecuzione.


Quando usare questo modello

Usalo quando:

  • I dati dell'utente non devono persistere
  • Le operazioni sono di breve durata (dai secondi ai minuti)
  • Debug
  • Questioni relative alla privacy
  • Questioni relative alla stabilità del carico
  • Vuoi risposta istantanea + elaborazione dello sfondo

Non usarlo quando:

  • Hai bisogno di una vera durata (usare una vera coda)
  • Le operazioni richiedono ore (usare Hangfire o simili)
  • È necessario esattamente-una volta semantica (usare una transazione distribuita)
  • È necessario coordinare più server (usare Redis o un broker di messaggi)

Conclusione -Effimera esecuzione come filosofia del design

Se Articolo LRU era circa imparare dimenticando, questo è circa esecuzione mediante evaporazione.

Il sistema ricorda esattamente abbastanza per essere utile - e niente di più.

E'

  • limitato
  • deterministico
  • privato
  • debuggabile
  • recuperabile
  • autopulente
  • robusto
  • e architettura-first

La cosa migliore? Sai già come implementarlo: un TaskCompletionSource, a Channel, a IMemoryCache con scadenza scorrevole, e un lavoratore di background.

Fuoco... e non dimenticarti.


Cosa c'e' dopo?

Dentro **Parte 2: Costruire una biblioteca di esecuzione effimera riutilizzabile**Trasformeremo questo schema in un aiutante drop-in:

  • EphemeralForEachAsync<T> - come Parallel.ForEachAsync ma con monitoraggio delle operazioni
  • Gasdotti a chiave per l'esecuzione sequenziale per entità
  • EphemeralWorkCoordinator<T> - una coda di lavoro osservabile a lunga durata
  • Coordinatori nominati/digitati con AddEphemeralWorkCoordinator<TCoordinator> (come AddHttpClient)
  • Full DI integrazione con una durata di vita determinata e singleton
  • Confronto con altri approcci (flusso dati TPL, canali, servizi di background)

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.