This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Friday, 12 December 2025
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:
Gli sviluppatori ASP.NET tendono a scegliere tra tre opzioni sbagliate:
Tu Task.Run() Qualcosa, sperare che finisca, e quando esplode non ottieni niente. Nessuna correlazione, nessuna ricostruzione, nessuna idea di cosa abbia fallito.
Blocchi i fili che non dovresti bloccare, il flusso muore, la latenza esplode, i tuoi utenti ti odiano.
Ora hai:
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':
Una memoria "appena abbastanza lunga" di ciò che il sistema sta facendo - e niente di più.
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:
...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
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:
Task<TaskCompletion>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.
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;
}
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:
Task al chiamante immediatamenteE' il ponte tra "fuoco-e-dimenticato" e "fuoco-e-traccia."
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.
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:
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.
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à:
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 bloccareIsCompleted SondaggioIsFaulted per vedere se è fallitoResult per ottenere il contenuto tradottoQuando 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;
}
}
.NET's Task ha alcune strane combinazioni di stato:
IsCompleted è vero per entrambi Completamento riuscito E compiti difettosiIsCompletedSuccessfully è vero solo per il successoIsFaulted significa che ha lanciato un'eccezioneIsCanceled significa che è stato cancellatoQuindi devi controllare. IsFaulted prima del controllo IsCompleted, o tratterai i fallimenti come successi.
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:
"none")Questo e' Autoregolazione dei sondaggi - si ferma automaticamente quando non c'è niente da guardare.
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é:
Questa è l'architettura Vorrei che browser e frontend framework avessero utilizzato per le operazioni di sessione.
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.
Usalo quando:
Non usarlo quando:
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'
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.
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 operazioniEphemeralWorkCoordinator<T> - una coda di lavoro osservabile a lunga durataAddEphemeralWorkCoordinator<TCoordinator> (come AddHttpClient)© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.