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
Fire et Don't Bien sûr. Forget - Exécution provisoire sans état
Friday, 12 December 2025
La plupart des systèmes asynques se souviennent soit trop de (logs, queuesM SK2 jets persistants que vous n'avez jamais voulus )…, soit ils ne se souvient rien du tout.
Cet article présente quelque chose de différent:
Un modèle où chaque opération async devient une petite Traçabilité, d'inspection, re-retrievable, auto-unité de tâche de nettoyage -et l’ensemble du système demeure privé , boundedM SK2 rapide, et déterministeMSC4
Il s’agit de mon article précédent, ,. Apprentissage des LRU - Lorsque l’excès de capacité rend votre système meilleur. On a étudié comment la durée de l’expiration des bouées devient un mécanisme de survie.
Ce dernier explore l’autre moitié. bounded, exécution éphémère - comment un petit tampon de roulement des tâches actives devient un débugger , un registre d’événements , et un moteur de replay M SK3 sans tenir des données sur les utilisateurs
L'exemple provient de mon blog's widget de traduction -une petite interface pour la traduction du contenu de marquage à l'aperçu sur -leM SK3fléchanceMSC4 Il semble trivialMska5 Sous le capuchonM Ska6 il met en œuvre des patrons que vous pouvez voler pour tout flux de travail asyncMka7
Il s'agit de Partie 1 d’une série de deux pièces, - et :
- Partie 1: La théorieM SK1 le modèle , et un exemple réel de monde
- Partie 2: Établissement d'une bibliothèque réutilisable d'exécution temporaire: La mise en œuvre complète avec l’intégration de DI
NUGET!!!
Cela est maintenant dans le mostlylucid.ephemerals Nuget paquet aussi plus que 20 les patrons éphémères de la plupart du temps clairs et 'atoms.
Les fichiers de source
Le code source complet se trouve dans le mostlylucid.atoms GitHub repository
Le problème -Async Workflows Créer un état caché
ASP.Les développeurs de NET ont tendance à choisir parmi trois mauvaises options
Fire-and-forget
Vous Task.Run() quelque chose, espérer qu’il se termine , et quand il explose vous n’obtiendrez rienMSC2 Aucune corrélationM SK3 aucune reconstruction, aucune idée de ce qui a échoué
Le feu-etM SK1attendre
Vous bloquez les threads que vous ne devriez pas bloquer't block. Le débit d’alimentation décède . La latence exploseM SK3 Vos utilisateurs te chassentMSC4
Couloirs/channels partout
Vous avez maintenant:
- des listes non limitées
- État persistant que vous n’avez pas voulu conserver
- PII est accidentellement entreposé quelque part
- un système distribué où vous voulez simplement une simple opération
Traçage distribué
Utilisable… mais externeM SK1 Également : il peut' ne jouer rien et souvent détruire des données que vous n'avez jamais voulu stockerMSC4
Ce que nous voulons en réalité, c’est
- Short-lived
- énergie privée
- débugable
- d'inspection
- bounded
- retrievable si nécessaire
- et ensuite disparus
Une mémoire "justement assez longueM SK1 de ce que le système fait -et rien d'autre.
Le modèle -Fire et Don't Quite Forget (en anglais seulement)
Voici l’idée en une seule phrase.
Démarrer immédiatement une opération asynque, la suivre explicitement par l'entremise d'une source de tâcheCompletionSource, le nettoyer de façon déterministe , et conserver un petit tampon roulant des dernières tâches afin que vous puissiez les inspecter ou les récupérer.
Il se trouve à mi-chemin entre:
- Source des événements
- traçage
- Enquête sur les emplois
- futures/promises
- Sessions temporaires
…sans qu’il ne devienne l’une ou l’autre d’entre elles .
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
Anatomie d'une tâche éphémère
La structure de base des données est une TranslateTask -une petite unité d'exécution atomique appuyée par une 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);
Il contient:
- Un ID unique ( correspond à l’ensemble du flux )
- Un indicateur de temps (au moment où il a commencéM SK1
- Données métadonnées ( langage -sans contenu de l'utilisateur!)
- Une référence à la
Task<TaskCompletion> - Durée ( calculée à l’aide d’un accès )
- état d’erreur ( dérivée de l’état des tâches)
- Résultat final (seulement si rempli avec succèsM SK1
Que ce n’est pas le cas.
AWS a des fonctions étapenelles. Azure a des fonctionnalités durables . Vous disposez de lignes de code qui ne nécessitent pas une facture en nuage'
Modèle de source d'achèvement des tâches -Bridging Fire-andM SK2Forget to Fire -and+-Track
La magie se produit dans le BackgroundTranslateService. Au lieu du feu, nous utilisons un Source d'achèvement de la tâche -une promesse -construction similaire qui nous permet de revenir immédiatement alors que le travail se déroule dans l'arrière-plan.
// 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;
}
Quelle source de clôture d'une tâche est-elle?
Si vous n’avez pas ' utilisé TaskCompletionSource<T> avant de le considérer comme un Je vous promets de contrôler manuellement. Contrairement à un ordinaire Task qui se termine lorsque son travail est terminé, un SCT s’achève lorsque vous établissement SetResult(), SetException(), ou SetCanceled().
Cela vous permet:
- Retourner un
Taskà l'appelant immédiatement - Faire le travail réel ailleurs (fréquemment threadM SK1service de fond, etcMSC3
- Remplir la tâche lorsque vous Décider qu’elle est terminée
Il s’agit de la passerelle entre ", feu, - et -, oublier " et ", feu.
La couche de l'API -Le lancement d'une traduction
Lorsqu'un utilisateur clique sur "Translate",, l'interface appelle 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 retourne immédiatement avec un seul ID de tâche:
// 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 réponse est instantanée. La traduction réelle se fait en arrière-plan. L'utilisateur peut faire un sondage ou simplement regarder la mise à jour de l'interface.
La fenêtre d'opération de roulement -A Self-Cleaning Buffer
Le principal point de vue tiré du Article LRU s’applique aussi ici:
Gardez juste assez d'historique pour débuger et expliquer le système… et laissez la détérioration naturelle supprimer tout le reste.
// 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; }
}
}
Chaque nouvelle tâche:
- est ajouté à la fenêtre de roulement de l'utilisateur'
- Expulse le plus vieux si nous 're à la capacité (5 maxM SK2
- Évaporation totale après 6 heures (absolueM SK2 ou 1 heure d'inactivité SSK4sliding)
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
Aucun risque de conservation. Aucun PII dans le cache (juste les ID des tâchesM SK2 taches horairesMST3 et codes linguistiquesMSC4 Pas d'erreur en matière d'application de la GDPR MST5
Concurrence limitée -The Governor Loop
Le service de base met en œuvre un coïncidence fixe le cycle en utilisant un lecteur de canaux et 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");
}
}
}
Il s’agit d’un Gouverneur d'un moteur à vapeur: plus de charge → accroissement de la pression de l'arrière
Le modèle vous donne:
- Aucune spirale de surcharge -can't spawn unlimited tasks
- Modèle de comportement du temps réel - latence limitée
- Élimination naturelle des éruptions -le canal tamponne les pics
- Stability under load - se dégrade gracieusement
Compléter la source de fin des tâches
Lorsque le travail de traduction est terminé, nous complétons la 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);
}
}
L’appelant's Task<TaskCompletion> -qui ont été reçus immédiatement lorsqu’ils ont appelé Translate() -actuellement s’achève. Ils peuventM SK2
awaitsi elles veulent bloquer- Vérifier
IsCompletedd'enquêter - Vérifier
IsFaultedpour voir s'il a échoué - Vérifier
Resultpour obtenir le contenu traduit
La projection de l’état -État dérivant de l'État de tâche
Lors de l’affichage des tâches à l’utilisateur,, nous projetons l’état de la tâche en direct dans un modèle de vue:
// 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;
}
}
Note sur l'état de la tâche
.NET's Task a quelques combinaisons d’états étranges:
IsCompletedest vrai pour les deux exécution réussie ET tâches défectueusesIsCompletedSuccessfullyc'est seulement vrai pour le succèsIsFaultedsignifie qu'il a lancé une exceptionIsCanceledsignifie qu'il a été annulé
Vous devez donc vérifier IsFaulted avant la vérification IsCompleted, ou vous ' traitez les défaillances comme des succèsM SK2
L’interface -HTMX Poll
Les sondages de vue pour les mises à jour utilisant HTMX's 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>
Le bit intelligent: hx-trigger="@trigger" changements fondés sur l’état:
- Si les tâches sont encore en cours: sondage tous 5 secondes
- Si toutes les tâches sont accomplies: cesser de voter M SK1
"none")
Il s'agit de auto-vote réglementaire -il s’arrête automatiquement lorsqu’il n’y a pas de choses à regarder
Récupération du résultat
Lorsque l'utilisateur clique sur "View", nous obtenons la traduction complétée
// 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);
}
Le JavaScript remplit ensuite l’éditeur:
// 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);
});
}
Pourquoi cela est une protection de la vie privée-Safe
Parce:
- Aucun contenu d'utilisateur n'est stocké dans le cache -métadonnées de tâches seulement (ID,marquage du tempsM SK3 langageMSC4
- Le contenu n’existe que dans le résultat de Task'. -qui est en mémoire, attaché à la tâche
- La fenêtre est limitée -max 5 tâches par utilisateur, max 6 retenue des heures
- Tout se décompose naturellement -sliding expiration nettoie les utilisateurs inactifs
- Rien ne touche le disque -pas de registresM SK1 pas de couloirs , aucune base de données
- Vous ne pouvez pas retenir accidentellement PII -il y a 'il n'y a pas de place pour aller
C'est l'architecture J'aimerais que les navigateurs et les cadres de front-end aient été utilisés pour les opérations de session.
Le comportement émergent
Ce que vous obtiendrez sans trop réfléchir:
| Avantage MSC1 Comment SMC2 | |
|---|---|
| La stabilité | Compétence limitée par voie de canal + Modèle sémaphore |
| Rétroaction négative | Plus de charge → pression d'arrière MSC2 mémoire limitée MSC3 comportement cohérent SMC4 |
| Visibilité de débugging | Les opérations exactes sont présentes assez longtemps pour être inspectées. |
| Récupérabilité | Les résultats obtenus sont accessibles jusqu’à leur expiration |
| Rétention de données zéro | La protection des renseignements personnels et la simplicité sont alignées pour une fois |
| auto-optimisation de l'exécution | Les tâches anciennes disparaissent, celles pertinentes restent |
Tout comme le cache LRU a affiné la mémoire comportementale,, la fenêtre d’opérations de roulement affine la vue d’exécution.
Quand utiliser ce modèle
Utilisez-le lorsque:
- Les données de l'utilisateur ne doivent pas persister
- Les opérations sont courtes-livres (de secondes à minutesM SK2
- Questions de débugging
- Questions de protection des renseignements personnels
- Questions liées à la stabilité de charge
- Vous voulez une réponse instantanée + traitement de fond
Ne l’utilisez pas lorsque
- Vous avez besoin d'une durabilité réelle (utilisez une queue réelle)
- Les opérations prennent des heures (utiliser Hangfire ou similaireM SK1
- Vous avez besoin de la sémantique exacte-une seule fois M SK1utiliser une transaction distribuée)
- Plusieurs serveurs doivent coordonner (utiliser Redis ou un courtier de messages)
Conclusion -Exécution temporaire comme philosophie de conception
Si le Article LRU a été l'apprentissage en oublient, cette est sur exécuter par évaporation.
Le système se souvient exactement assez pour être utile -et rien de plusM SK1
Il n’y a pas de différence entre le nombre d’heures de travail et la durée des heures de travail.
- bounded
- déterministe
- énergie privée
- débugable
- retrievable
- auto- nettoyant
- intégralité
- et architecture-première
La meilleure chose? Vous savez déjà comment la mettre en œuvre: a TaskCompletionSource, a Channel, an IMemoryCache avec échéance de sliding, et un travailleur d’arrière-plan.
Fire… et DonM SK1t Quite Forget.
Qu’est-ce que's Suivant?
À l'intérieur Partie 2: Établissement d'une bibliothèque réutilisable d'exécution temporaire, nous ' ferons de ce modèle une chuteM SK2 dans l’aideur:
EphemeralForEachAsync<T>- commeParallel.ForEachAsyncmais avec le suivi des opérations- Lignes de conduite clés pour l'exécution séquentielle d'une entité per-
EphemeralWorkCoordinator<T>- une longue queue de travail observable- Nommés/coordonnateurs typés avec
AddEphemeralWorkCoordinator<TCoordinator>(commeAddHttpClient) - Intégration complète du DI avec des durées de vie de portée et d'un seul ton
- Comparaison avec d'autres approches (TPL Dataflow , ChannelsM SK2 Background Services)
Liens
- Apprentissage des LRU - Lorsque l’excès de capacité rend votre système meilleur - l'article complémentaire sur la mémoire limitée
- Partie 2: Établissement d'une bibliothèque réutilisable d'exécution temporaire -la mise en œuvre complète
- Documentation de source d'achèvement des tâches -Microsoft's documents sur le modèle TCS
- Système.Threading.Channels -le producteurM SK1les consommateurs primitives que nous utilisons
- Documentation IMemoryCache -ASP.NET's construitsM SK3dans le cache