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
Eld och gör det inte Ganska Oförgätentlig avrättning utan tillstånd
Friday, 12 December 2025
De flesta async-system kommer antingen ihåg för mycket (loggar, köer, ihållande skräp du aldrig ville ha)... eller så minns de ingenting alls (eld-och-glömma svarta hål som försvinner när något går fel).
Denna artikel introducerar något annat:
Ett mönster där varje async-operation blir en liten Spårbar, inspekteras, återinsättningsbar, självrengörande arbetsenhet -och hela systemet förblir privat, avgränsat, snabbt och deterministiskt.
Det är följeslagaren till min tidigare artikel, Lära LRU - När överträffa kapacitet gör ditt system bättre. Att man utforskade hur avgränsat minne + glidande utgång blir en överlevnadsmekanism.
Den här utforskar den andra halvan: avgränsad, flyktig avrättning -hur en liten rullande buffert av aktiva uppgifter blir en avlusare, en händelselogg, och en replay motor, utan att behålla några användardata.
Exemplet kommer från min bloggs översättning widget - en liten UI för att översätta markdown innehåll i farten. Det ser trivialt ut. Under huven, det implementerar mönster som du kan stjäla för alla async arbetsflöden.
Det här är Häfte 1 i en serie med två delar:
- Del 1 (denna artikel): Teorin, mönstret och ett verkligt exempel
- Del 2: Att bygga ett återanvändbart efemärt avrättningsbibliotek: Fullständigt genomförande med integrering av DI
NUGET!!!
Problemet -Async Workflows skapa dolda tillstånd
ASP.NET utvecklare tenderar att välja mellan tre dåliga alternativ:
Eld-och-glömma
Du där. Task.Run() När den sprängs får man inget samband, ingen rekonstruktion, ingen aning om vad som misslyckades.
Eld-och-vänta
Genomströmning dör, latens exploderar och användarna hatar dig.
Köer/kanaler överallt
Och nu har du:
- obundna köer
- Ihållande tillstånd du inte menade att hålla
- PII förvaras av misstag någonstans
- ett distribuerat system där du bara ville ha en enkel operation
Distribuerad spårning
Användbar... men extern. Dessutom: det kan inte spela upp något och ofta läcker data du aldrig tänkt att lagra.
Vad vi egentligen vill ha är:
- Kortlivad
- privat
- Avlusningsbar
- inspekteras
- avgränsad
- återanvändbar vid behov
- och sedan borta
Ett "bara tillräckligt långt" minne av vad systemet gör - och inget mer.
Mönstret - Eld och Inte riktigt Glöm
Här är idén i en mening:
Starta en async-operation omedelbart, spåra den explicit via en TaskCompletionSource, rensa upp den deterministiskt, och hålla en liten rullande buffert av de senaste uppgifterna så att du kan inspektera eller hämta dem - utan att behålla något användarinnehåll.
Det sitter halvvägs mellan:
- Händelseinköp
- spårning
- jobbköer
- futures/promises
- Efemära sessioner
...utan att bli någon av dem.
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
Anatomi av en efemär uppgift
Den centrala datastrukturen är en TranslateTask -en liten atomär avrättningsenhet som backas upp av en 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);
Den innehåller:
- Ett unikt ID (korrelerar över hela flödet)
- En tidsstämpel (när det började)
- Metadata (språk - inte användarinnehåll!)
- En hänvisning till det faktiska
Task<TaskCompletion> - Varaktighet (beräknad på tillträde)
- Feltillstånd (härlett från uppgiftstillstånd)
- Slutresultat (endast om det har slutförts med lyckat resultat)
Inget kvarstod, inget skrivet till diskett, inget lagrat utanför ett rullande fönster.
AWS har stegfunktioner. Azure har hållbara funktioner. Du har ... 30 rader kod som inte kräver en molnräkning.
AktivitetskompletteringSource Mönster -Bridning Fire-and-Glöm till Fire-and-Track
Magin händer i BackgroundTranslateService. I stället för eld-och-glömma, använder vi en Källa för slutförande av uppgift -en löfteslik konstruktion som låter oss återvända omedelbart medan arbetet sker i bakgrunden.
// 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;
}
Vad är en uppgiftskompletteringskälla?
Om du inte har använt TaskCompletionSource<T> före, se det som en lova att du styr manuellt. Till skillnad från en vanlig Task som slutför när dess arbete avslutas, en TCS färdig när du samtal SetResult(), SetException(), eller SetCanceled().
Detta låter dig:
- Returnera en
Tasktill den som ringer omedelbart - Gör själva arbetet någon annanstans (olika tråd, bakgrundstjänst, etc.)
- Fyll i uppgiften när du bestämma att det är gjort
Det är bron mellan "eld-och-glöm" och "eld-och-spår".
API-skiktet - Starta en översättning
När en användare klickar på "Translate" anropar gränssnittet 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'
});
});
API:et returneras omedelbart med bara ett aktivitets-ID:
// 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);
}
Svaret är omedelbart. Den faktiska översättningen körs i bakgrunden. Användaren kan välja eller bara titta på UI- uppdateringen.
Rullande drift fönster - En självrengörande buffer
Den viktigaste insikten från LRU-artikel gäller även här:
Håll bara tillräckligt med historia för att felsöka och resonera om systemet... och låta naturligt förfall radera allt annat.
// 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; }
}
}
Varje ny uppgift:
- Läggs till i användarens rullande fönster
- Trycker ut den äldsta om vi har kapacitet (5 max)
- Avdunstar helt efter 6 timmar (absolut) eller 1 timmes inaktivitet (glidning)
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
Ingen risk för retention. Ingen PII i cache (bara uppgifts-ID, tidsstämplar och språkkoder). Ingen GDPR-huvudvärk.
Gränslös konkurs Guvernör Loop
Bakgrundstjänsten implementerar en Fast överensstämmelse loop med hjälp av en kanalläsare och 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");
}
}
}
Det här är som en Guvernör på en ångmaskin: mer belastning → mottryck bygger → naturlig strypning → stabilitet.
Mönstret ger dig:
- Ingen överbelastning spiral -kan inte skapa obegränsade uppgifter
- Mjukt beteende i realtid -begränsad latens
- Naturlig utjämning av sprickbildningar -kanal buffertar spikarna
- Stabilitet under belastning - Försämrar graciöst.
Fullbordande av aktivitetskompletteringskällan
När det faktiska översättningsarbetet är avslutat, slutför vi 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);
}
}
Den som ringer Task<TaskCompletion> -som de fick direkt när de ringde Translate() - Nu är de färdiga.
awaitdet om de vill blockera- Kontrollera
IsCompletedtill undersökning - Kontrollera
IsFaultedför att se om den misslyckades - Kontrollera
Resultför att få det översatta innehållet
Statusprojicering -Deriving stat från Task State
När vi visar uppgifter för användaren projicerar vi liveuppgiftstillståndet i en vymodell:
// 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;
}
}
En anmärkning om uppgiftsstat
.NET:s Task har några udda tillståndskombinationer:
IsCompletedär sant för båda framgångsrikt slutförande OCH felaktiga uppgifterIsCompletedSuccessfullygäller endast för framgångIsFaultedbetyder att det kastade ett undantagIsCanceledbetyder att det ställdes in
Så du måste kolla IsFaulted Före kontroll IsCompletedAnnars behandlar du misslyckanden som framgångar.
UI -HTMX-mätningen
Vyundersökningar för uppdateringar med hjälp av 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>
Den smarta biten: hx-trigger="@trigger" Förändringar baserade på tillstånd:
- Om uppgifter fortfarande körs: opinionsundersökning var femte sekund
- Om alla uppgifter är gjorda: stoppa röstningen (
"none")
Det här är Självreglerande röstning -Det slutar automatiskt när det inte finns något att titta på.
Hämta resultatet
När användaren klickar på "Visa" hämtar vi den färdiga översättningen:
// 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);
}
JavaScriptet fyller sedan i editorn:
// 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);
});
}
Varför detta är integritetssäkert
Därför att:
- Inget användarinnehåll lagras i cache -endast aktivitetsmetadata (ID, tidsstämpel, språk)
- Innehåll finns bara i Aktivitetens resultat -som är till minne, knuten till uppgiften
- Fönstret är avgränsat -max 5 uppgifter per användare, max 6 timmar kvar
- Allt förfaller naturligt -glidande utgångsdatum rensar upp inaktiva användare
- Inget vidrör disken - Inga loggar, inga köer, ingen databas
- Du kan inte av misstag behålla PII - Det finns ingenstans att ta vägen.
Det här är arkitekturen. Jag önskar att webbläsare och frontendramar hade använts för sessionsoperationer.
Det nya beteendet
Vad du får utan att tänka för mycket:
VÄLKOMMEN FRÅN FLYGPLATSEN |---------|-----| | Stabilitet till Gränsad konvergens via kanal + semaformönster | Negativ återkopplingsslinga Mer belastning → mottryck → avgränsat minne → konsekvent beteende | Felsökningssynlighet Den exakta verksamheten är närvarande bara tillräckligt länge för att inspektera | Återvinningsförmåga Slutförda resultat är tillgängliga tills de upphör att gälla | Noll lagring av uppgifter med integritet och enkelhet anpassad för en gångs skull | Självoptimerande genomförande på gamla uppgifter faller bort, relevanta förblir med
Precis som LRU-cachen skärpte beteendeminnet skärper det rullande driftfönstret körvyn.
När du ska använda detta mönster
Använd den när:
- Användardata får inte kvarstå
- Verksamheten är kortlivad (sekunder till minuter)
- Avlusning av ärenden
- Sekretessfrågor
- Belastningsstabilitetsfrågor
- Du vill ha omedelbar respons + bakgrundsbehandling
Använd den inte när:
- Du behöver sann hållbarhet (använd en riktig kö)
- Verksamheten tar timmar (använd Hangfire eller liknande)
- Du behöver exakt en gång semantik (använd en distribuerad transaktion)
- Flera servrar måste samordna (använd Redis eller en meddelandemäklare)
Slutsats -Ephemeral Execution as a Design Philosophy
Om inte annat anges ska LRU-artikel var ungefär Att lära sig genom att glömma, Den här handlar om Genomförande genom avdunstning.
Systemet kommer ihåg exakt tillräckligt för att vara till hjälp - och inget mer.
Det är:
- avgränsad
- Deterministisk
- privat
- Avlusningsbar
- återanvändbar
- självrengörande
- robust
- och arkitektur-först
Du vet redan hur man genomför det: en TaskCompletionSource, , , , . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Channel, en IMemoryCache med glidande utgång, och en bakgrundsarbetare.
Eld... och glöm inte.
Vad är nästa?
Till **Del 2: Att bygga ett återanvändbart efemärt avrättningsbibliotek**Vi förvandlar mönstret till en drop-in-hjälpare.
EphemeralForEachAsync<T>- Som jag sa.Parallel.ForEachAsyncmen med driftspårning- Nyckelrörledningar för utförande per enhet i följd
EphemeralWorkCoordinator<T>- en långlivad observerbar arbetskö- Namngivna/typade samordnare med
AddEphemeralWorkCoordinator<TCoordinator>(somAddHttpClient) - Fullständig DI-integration med räckvidd och singleton-livslängder
- Jämförelse med andra metoder (TPL Dataflow, Kanaler, Bakgrundstjänster)
Länkar
- Lära LRU - När överträffa kapacitet gör ditt system bättre - följeslagaren artikeln om avgränsat minne
- Del 2: Att bygga ett återanvändbart efemärt avrättningsbibliotek -det fullständiga genomförandet
- Dokumentation om aktivitetskompletteringKälla -Microsofts dokument på TCS-mönstret.
- System. Threading.Channels -den producent-konsument primitiva vi använder
- ImoryCache-dokumentation -ASP.NETs inbyggda cache