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 mayoría de los sistemas async recuerdan demasiado (logs, colas, basura persistente que nunca quisiste)... o no recuerdan nada en absoluto (agujeros negros de fuego y olvido que desaparecen en el momento en que algo sale mal).
Este artículo introduce algo diferente:
Un patrón donde cada operación asíncrona se convierte en una diminuta rastreable, inspeccionable, recuperable, Unidad de tareas de auto-limpieza -y todo el sistema permanece privado, limitado, rápido y determinista.
Es el compañero de mi artículo anterior, **Aprender LRUs - Cuando la superación de la capacidad mejora su sistema**Que uno exploró cómo la memoria limitada + la expiración deslizante se convierte en un mecanismo de supervivencia.
Esta explora la otra mitad: ejecución efímera limitada -cómo un pequeño búfer móvil de tareas activas se convierte en un depurador, un registro de eventos y un motor de repetición, sin guardar ningún dato del usuario.
El ejemplo viene del widget de traducción de mi blog - una pequeña interfaz de usuario para traducir contenido Markdown sobre la marcha. Parece trivial. Bajo el capó, implementa patrones que puedes robar para cualquier flujo de trabajo asíncrono.
Esto es Parte 1 de una serie de dos partes:
Esto está ahora en el paquete Nuget mayoritariamente lucid.efemerals también más de 20 patrones mayoritariamente lucid.efemerals y 'átomos'.
Los desarrolladores de ASP.NET tienden a elegir entre tres malas opciones:
Tú Task.Run() algo, espero que termine, y cuando explota no consigues nada. Sin correlación, sin reconstrucción, ni idea de lo que falló.
Bloqueas hilos que no deberías bloquear, el rendimiento muere, la latencia explota, tus usuarios te odian.
Ahora tienes:
Útil... pero externo. Además: no puede reproducir nada y a menudo filtra datos que nunca tuvo la intención de almacenar.
Lo que realmente queremos es:
Un "lo suficientemente largo" memoria de lo que el sistema está haciendo - y nada más.
Esta es la idea en una frase:
Iniciar una operación asíncrona inmediatamente, rastrearla explícitamente a través de una TaskCompletionSource, limpiarla determinísticamente y mantener un pequeño búfer de rodadura de las últimas tareas para que pueda inspeccionarlas o recuperarlas, sin retener ningún contenido de usuario.
Se encuentra a mitad de camino entre:
...sin convertirse en ninguno de ellos.
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 estructura básica de los datos es una TranslateTask -una pequeña unidad de ejecución atómica respaldada por 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>Nada ha persistido, nada escrito en disco, nada almacenado más allá de una ventana móvil.
AWS tiene funciones de paso. Azure tiene funciones duraderas. Tienes... 30 líneas de código que no requieren una factura de nube.
La magia sucede en el BackgroundTranslateService. En lugar de fuego-y-olvidar, usamos un TaskCompletionSource -una construcción prometedora que nos permite regresar inmediatamente mientras el trabajo ocurre en el fondo.
// 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;
}
Si no has usado TaskCompletionSource<T> antes, pensar en ello como un Promete que controlas manualmente. A diferencia de un regular Task que se completa cuando su trabajo termina, un TCS se completa cuando tú Llamada SetResult(), SetException(), o SetCanceled().
Esto le permite:
Task al que llama inmediatamenteEs el puente entre "fuego y olvido" y "fuego y rastro".
Cuando un usuario hace clic en "Traducir", la interfaz llama a la 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'
});
});
La API devuelve inmediatamente con sólo un ID de tarea:
// 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 respuesta es instantánea. La traducción real se ejecuta en segundo plano. El usuario puede hacer una encuesta o simplemente ver la actualización de la interfaz de usuario.
La perspicacia clave de la Artículo LRU se aplica también en este caso:
Mantenga la historia suficiente para depurar y razonar sobre el sistema... y dejar que la decadencia natural borre todo lo demás.
// 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; }
}
}
Cada nueva tarea:
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
No hay riesgo de retención. No hay PII en la caché (sólo identificaciones de tareas, marcas de tiempo y códigos de idioma).
El servicio de antecedentes aplica un Condición fija bucle usando un lector de canales y 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");
}
}
}
Esto es como un gobernador en una máquina de vapor: más carga → contrapresión construye → estrangulamiento natural → estabilidad.
El patrón le da:
Cuando termine el trabajo de traducción real, completamos el 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);
}
}
La persona que llamó Task<TaskCompletion> - que recibieron inmediatamente cuando llamaron Translate() - Ahora se completan.
await si quieren bloquearloIsCompleted a encuestaIsFaulted para ver si falló.Result para obtener el contenido traducidoAl mostrar tareas al usuario, proyectamos el estado de tarea en vivo en un modelo de vista:
// 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 tiene algunas combinaciones peculiares de estado:
IsCompleted es cierto para ambos Finalización exitosa Y tareas fallidasIsCompletedSuccessfully sólo es verdad para el éxitoIsFaulted significa que lanzó una excepciónIsCanceled significa que se canceló.Así que debes comprobarlo. IsFaulted antes de comprobar IsCompleted, o tratarás los fracasos como éxitos.
Las encuestas de visualización para las actualizaciones utilizando 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 inteligente: hx-trigger="@trigger" cambios basados en el estado:
"none")Esto es votación autorreguladora - se detiene automáticamente cuando no hay nada que ver.
Cuando el usuario hace clic en "Ver", se obtiene la traducción completa:
// 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);
}
El JavaScript entonces pobla el 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);
});
}
Porque:
Esta es la arquitectura Desearía que los navegadores y los marcos de interfaz se hubieran utilizado para las operaciones de sesión.
Lo que se obtiene sin pensar demasiado:
|---------|-----| | Estabilidad Condición limitada vía canal + patrón de semáforo | Lazo de retroalimentación negativo Más carga → presión de espalda → memoria limitada → comportamiento consistente | Visibilidad de depuración Las operaciones exactas están presentes el tiempo suficiente para inspeccionar | Capacidad de obtención Resultados completados son accesibles hasta que expiran | Cero retención de datos Privacidad y simplicidad alineadas por una vez | Ejecución autooptimizada Las viejas tareas caen, las relevantes permanecen.
Al igual que la memoria caché LRU agudizó la memoria de comportamiento, la ventana de operación de rodadura agudiza la vista de ejecución.
Úselo cuando:
No lo uses cuando:
Si la Artículo LRU era sobre aprender olvidando, esta es sobre ejecutándose mediante evaporación.
El sistema recuerda exactamente lo suficiente para ser útil - y nada más.
Es:
¿Lo mejor? Ya sabes cómo implementarlo: un TaskCompletionSource, a Channel, un IMemoryCache con la expiración deslizante, y un trabajador de fondo.
Fuego... y no lo olvides.
In Parte 2: Construcción de una biblioteca de ejecución efímera reutilizable, vamos a convertir este patrón en un ayudante:
EphemeralForEachAsync<T> - como Parallel.ForEachAsync pero con seguimiento de operaciónEphemeralWorkCoordinator<T> - una cola de trabajo observable de larga duraciónAddEphemeralWorkCoordinator<TCoordinator> (como AddHttpClient)© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.