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 Don't Ganska Glöm - Öfmjukelse utan stat
Friday, 12 December 2025
De flesta async-system kommer antingen ihåg för mycket (logs, köer , kvarlevda skräp du aldrig ville ha, )… eller så kommer de inte ihåg någonting alls. | ( | eld | - | och - | glöm svarta hål som försvinner när något går fel
Denna artikel introducerar något annorlunda
Ett mönster där varje async operation blir en liten spårbar, kontrollerbart, re-avtagbar, själv-tvättande arbetsenhet - och hela systemet stannar privatt , begränsat, , snabbt,
' är min tidigare artikels medföljare. Att lära sig LRUs - När överskott gör ditt system bättre. Att man utforskade hur ett begränsat minne + glittrande utplåning blir en överlevnadsmekanism
Den här utforskar den andra halvan bounded, tidiga straff - hur en liten rollande buffer av aktiva uppgifter blir en debugger , en händelselog, , och en återspelningsmaskin
Det exemplet kommer från min blogg's översättningsguidan | - | en liten gränssnitt för att översätta markerdown innehåll på |- | the |- | fly | . | Det ser triviellt ut |
Det här är Del 1 av en serie med två delar
- Del 1 (den här artikelnM SK2: Teorin, ,, mönstret och ett verkligt exempel på världen
- Del 2: Att bygga en återanvändbar ephemeral Execution Library: Den fulla implementeringen med DI-integration
NUGET!!!
Detta är nu även i det mest uppenbara packaget Nuget mer än 20.
källdates
Den fulla källkoden finns i mostlylucid.atoms GitHubs arkiv
Problemet -Async arbetsflöden skapa dolda tillstånd
ASP.NET-utvecklare tenderar att välja mellan tre dåliga alternativ
"Fire" - och - "forget
Du Task.Run() något , hoppas att det slutar, och när det exploderar får du ingenting . Ingen korrelation M SK3 ingen rekonstruering MSC4 ingen aning om vad som misslyckades
"Fire" - - - och - - vänta
Du blockerar trådar du inte borde blockera
köer, /, kanaler överallt
Nu har du
- obegränsade köer
- ihärdlig tillstånd du inte tänkt att behålla
- PII lagrats oavsiktligt någonstans
- ett distribuerat system där man bara ville ha en enkel operation
Distribuerad spårning
användbar… men externt. också : den kan inte spela upp någonting igen och läcker ofta data som du aldrig hade tänkt att lagra
Vad vi egentligen vill är
- kort-liv
- hemliga
- lösbar
- kontrollerbart
- begränsat
- möjlig att hämta om det behövs
- och sedan var den borta
En """, bara tillräckligt lång, """, minne av vad systemet gör.
Pattern -Fire och Don't Klart Glöm
Här är idén i en mening.
Starta en async operation omedelbart, spåra den eksplicitt via en TaskCompletionSource
Det ligger halvvägs mellan
- eventsourcing
- att spåra
- jobb köer
- framtida löften
- tidiga sammankomster
… 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 tidig uppgift
Kerndatastrukturen är TranslateTask - en liten atomisk uträttelseenhet som stöds av 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);
Det innehåller:
- En unik ID ( korrelerar över hela strömmen )
- En tidsramp ( när den börjadeM SK1
- Metadata (språk
- En referens till den faktiska
Task<TaskCompletion> - Tydlighet (beräknat när man får tillgång till den
- Błędstaten (odlat från uppgiftsstaten)
- Slutresultat ( endast om färdigt framgångsrikt
Det ', det är det , ., inget överlevt, ., inget skrivet på disken, ., inget lagret bortom en rullande fönstertil
AWS har stegfunktioner. Azure har hållbara funktioner. Du har ♫ … ♫ ♫ 30 ♫ rader kod som inte kräver en molnbil ♫' ♫
TaskCompletionSource Pattern -Störning av eld - och-Ska bort att elda
Magien händer i BackgroundTranslateServiceIstället för eld använder vi Uppbyggnadskällan - ett löfte - som en 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 TaskCompletionSource , "?"?
Om du inte använt TaskCompletionSource<T> före "," tänkte man på det som en lova att du kontrollerar det manuell. Till skillnad från en vanlig Task som slutar när dess arbete slutar, en TCS slutar du ringa SetResult(), SetException(), eller SetCanceled().
Det här låter dig
- Ge tillbaka en
Tasktill telefonören omedelbart - Gör det faktiska arbetet någon annanstans (skiljbar tråd , bakgrundstjänsterM SK2 etc.)
- Gör uppgiften när du bestämma att "'" är gjort
Det är bryggan mellan ", brand, - och -, glömt " och ", brand.
API-layret -Starta en översättning
När en user klickar "Verta", så ropar frontend på 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: n kommer tillbaka omedelbart med bara en uppgift 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
Rolling Operationsfenster -A Self-Cleaning Buffer
Den viktigaste insikten från LRU-artikel gäller även här:
Håll bara tillräckligt med historia för att avbugera och resonera kring systemet… och låt naturlig förruttning 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:
- Är tilldana till user's rullande venster
- Springer ut den äldsta om vi
- Evaporeras helt efter 6 timmar (absolutt | ) | eller ♫ 1 | en timme av inaktivitet
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
Inget förvarande risk. Ingen PII i kaset ( bara uppgiftersidentifiseringar , tidstämningar M SK3 och språkkoder
Bounded Concurrency -The Governor Loop
Hintergrundtjänsterna implementerar fikst samtida partners loop med hjälp av en kanalreader 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 gaskraftverksledare: mer belastning → baktryck byggs ♫ ♫ → ♫ naturlig drivkraft ♫
Patternet ger dig
- Inga överlastspiraler -kan't skapa oändliga uppgifter
- Soft real-tidsbeteende -begränsad latens
- Naturligt glättande av sprängningar -kanal bufferar spetsen
- Stabilitet under last - bryter sig elegant
Completing of the TaskCompletionSource
När den faktiska översättningsarbeiten slutar
// 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);
}
}
Kallaren's Task<TaskCompletion> - som de fick direkt när de ringde Translate() - slutar nu . De kan
awaitom de vill blockera- Kontroller
IsCompletedatt rösta - Kontroller
IsFaultedför att se om det misslyckades - Kontroller
Resultför att få översatt innehåll
Statusprognosen -Deriving State from Task State
När vi visar upp uppgifter för användaren, projicerar vi den verkliga uppgifternas tillstånd i en modell
// 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 anteckning om arbetslägen
.NET's Task har några märkliga tillståndskombinationer:
IsCompletedär sant för båda framgångsrikt utförande och felaktiga uppgifterIsCompletedSuccessfullyär bara sant för framgångIsFaultedinnebär att det kastade en undantagIsCanceledbetyder att den har avgått
Så du måste kolla IsFaulted före att kolla IsCompleted, eller så ska du treatera misslyckanden som framgångar
Ui -HTMX Polling
Synundersökningar för uppdaterade med 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 uppgifterna fortfarande pågår
- Om alla uppgifter är utförda: sluta rösta (
"none")
Det här är självregulerande val - stannar automatiskt när det finns något att titta på
Att hämta utfallet
När användaren klickar "View", får vi slutgiltiga ö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);
}
JavaScript befolkar sedan redaktören:
// 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 är det här privat?
Eftersom:
- Inget användarinnehåll lagras i kaset - endast uppgifter för uppgifter
- innehållet existerar bara i uppgifternas resultat - som är i minnet, som ansluts till uppgifterna
- Oknet är begränsat -max 5 uppgifter per användare
- Allt bryts ner naturligt -sliding expiration cleans up inactive users
- Inget rör vid disket - inga logs , inga köer M SK2 ingen databas
- Du kan inte oavsiktligt behålla PII -, där, ', det finns inget ställe dit den ska gå
Det här är arkitekturen Jag önskar att webblader och frontend ramverk hade använts för sessioner.
Det uppväxande beteendet
Vad du får utan att tänka för hårt:
| nytta | Hur | ||||||
|---|---|---|---|---|---|---|---|
| Stabilitet | Begränsad samtida genom kanal + mönster av mönsters | ||||||
| Negativ återkopplingsloop | Mer belastning | → | baktryck | → | begränsad minne | МSK3 | konsekventa beteende |
| Debug-visibilitet | De exakta operationerna är närvarande tillräckligt länge för att inspektera | ||||||
| Lösningsbarhet | Completed results are accessible until they expire | ||||||
| Noll databevarande | Privatsphäre och enkelhet sammanlänkade för en gång | ||||||
| Self-optimering av uitvoering | Gamla uppgifter faller bort |
Precis som LRU-キャッシュen hade täckt beteendemässiga minnen, täcker rollandets operationsfenster inspelningsbilden.
När man ska använda det här mönstret
Använd den när:
- Användardata måste inte finnas kvar
- Operationer är korta
- Problemet löser
- Privatlivsfrågor
- Ladestabilitet
- Du vill ha en omedelbar respons + bakgrundsprocess
Använd inte den när
- Du behöver riktig hållbarhet (använd en riktig kö)
- Operationer tar timmar (använda Hangfire eller liknande )
- Du behöver exakt-en gång semantik | ( | använda en fördelad transaktion
- Flera server behöver koordinera (använda Redis eller ett meddelandesmakare)
slutsats -Permanent utföring som en designfilosofi
Om LRU-artikel var att lära sig genom att glömma, den här handlar om utförd genom att avdunsta.
Systemet kommer ihåg precis tillräckligt mycket för att vara hjälpsamt - och inget mer .
Det's:
- begränsat
- deterministisk
- hemliga
- lösbar
- lösbar
- själv-tvättande
- robust
- och arkitektur-first
Det bästa? Du vet redan hur du ska implementera det TaskCompletionSource, a Channel, an IMemoryCache med skiftig utgångsdatum , och en bakgrundsarbetare.
"Fire" … och "Don"
Vad's Nästa?
I Del 2: Att bygga en återanvändbar ephemeral Execution Library, vi ' ska göra detta mönster till en droppe
EphemeralForEachAsync<T>- somParallel.ForEachAsyncmen med operationsspårning- Keyed pipelines for per-entity sequential execution
EphemeralWorkCoordinator<T>- en lång- Namerade koordinatorer med "/".
AddEphemeralWorkCoordinator<TCoordinator>( somAddHttpClient) - Full DI integration med skalad och enton livslängd
- jämförelse med andra tillvägagångssätt (TPL Dataflow
Links
- Att lära sig LRUs - När överskott gör ditt system bättre -kommande artikel om begränsad minne
- Del 2: Att bygga en återanvändbar ephemeral Execution Library -fulla implementering
- Dokumentation för uppfyllda uppgifter -Microsoft's dokumentationer på TCS-mönstret
- System.Threading.Channel -tillverkaren-konsument primitiv vi använder
- IMemoryCache-dokumentation -ASP