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
Useimmat asynkkijärjestelmät joko muistavat liikaa (lokit, jonot, sinnikkäät roskat, joita et koskaan halunnut)... tai he eivät muista yhtään mitään (tulta ja unohda mustia aukkoja, jotka katoavat, kun jokin menee pieleen).
Tässä artikkelissa esitellään jotain erilaista:
Kaava, jossa jokaisesta asynkistä operaatiosta tulee pieni Jäljitettävä, Tarkastettu, Uudelleen noudettavissa, itsepuhdistava tehtäväyksikkö Koko järjestelmä pysyy yksityisenä, rajattuna, nopeana ja deterministisenä.
Se on aiemman artikkelini kumppani. **LRU:iden oppiminen - Kun suurempi kapasiteetti tekee järjestelmästäsi paremman**Tuo tutki, kuinka rajatusta muistista ja liukuvasta vanhenemisesta tulee selviytymismekanismi.
Tämä tutkii toista puolikasta: Rajoitettu, ohimenevä teloitus − Miten pieni liikkuva puskuri aktiivisista tehtävistä muuttuu vianetsintä-, tapahtumaloki- ja uusintamoottoriksi ilman, että käyttäjätietoja säilytetään.
Esimerkki on blogini käännöswidgetistä, joka on pieni käyttöliittymä maaliviivan sisällön kääntämiseen lentoon. Se näyttää vähäpätöiseltä. Huppujen alla se toteuttaa kuvioita, joita voi varastaa mihin tahansa asynkkiseen työnkulkuun.
Tämä on Osa 1 kaksiosaisesta sarjasta:
ASP.NET-kehittäjät valitsevat yleensä kolmesta huonosta vaihtoehdosta:
Sinä Task.Run() Toivottavasti se loppuu, ja kun se räjähtää, ei saa mitään.
Latenssi räjähtää ja käyttäjät vihaavat sinua.
Nyt sinulla on:
Hyödyllistä, mutta ulkoista. Se ei myöskään pysty toistamaan mitään ja vuotaa usein tietoja, joita ei ole koskaan tarkoitettu tallentamaan.
Itse asiassa haluamme, että:
"Vielä tarpeeksi pitkä" muisto siitä, mitä järjestelmä tekee, eikä mitään muuta.
Yhdessä lauseessa on ajatus:
Aloita async-operaatio välittömästi, seuraa sitä nimenomaan TaskCompletionSourcen kautta, puhdista se deterministisesti ja pidä pieni pyörimispuskuri viimeisissä tehtävissä, jotta voit tarkastaa tai noutaa ne – säilyttämättä mitään käyttäjäsisältöä.
Se istuu puolivälissä:
...tulematta keneksikään heistä.
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
Perusdatarakenne on TranslateTask -pieni atomin teloitusyksikkö, jonka tukena on 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);
Se sisältää:
Task<TaskCompletion>Mikään ei kestänyt, ei ollut kirjoitettua levylle, ei ollut tallennettuna pyörivän ikkunan taakse.
AWS:llä on Step Functions. Azurella on kestäviä toimintoja. Sinulla on 30 riviä koodia, jotka eivät vaadi pilvilaskua.
Taikuus tapahtuu BackgroundTranslateServicePalon ja unohduksen sijaan käytämme Tehtäväntäyttölähde − Luvatun kaltainen rakennelma, jonka avulla voimme palata välittömästi, kun työt tapahtuvat taustalla.
// 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;
}
Jos et ole käyttänyt TaskCompletionSource<T> ennen, ajattele sitä lupaa hallita manuaalisestiToisin kuin tavallinen Task joka valmistuu, kun sen työ päättyy, TCS valmistuu, kun sinä soitto SetResult(), SetException(), tai SetCanceled().
Näin voit:
Task soittajalle hetiSe on "tulen ja unohduksen" ja "tulen ja raidan" välinen silta.
Kun käyttäjä napsauttaa "käännä", etuosa kutsuu 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 palaa välittömästi pelkällä tehtävätunnisteella:
// 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);
}
Vastaus on instant. Varsinainen käännös kulkee taustalla. Käyttäjä voi tehdä kyselyjä tai vain katsoa UI-päivityksen.
Avainnäkemys: LRU:n artikkeli Koskee myös tätä asiaa:
Pidä vain sen verran historiaa, että voit selvittää järjestelmän ja antaa luonnollisen rappeutumisen pyyhkiä pois kaiken muun.
// 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; }
}
}
Jokainen uusi tehtävä:
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
Ei retentioriskiä. Ei PII:tä välimuistissa (vain tehtävätunnisteet, aikaleimat ja kielikoodit). Ei GDPR-päänsärkyä.
Taustapalvelu toteuttaa kiinteä vaihtovelkakirja silmukka käyttäen kanavanlukijaa ja 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");
}
}
}
Tämä on kuin kuvernööri höyrymoottorilla: lisää kuormaa → vastapainetta rakentaa → luonnollista thratting → vakautta.
Kaava antaa sinulle:
Kun varsinainen käännöstyö päättyy, suoritamme TCS:n:
// 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);
}
}
Soittajan Task<TaskCompletion> - Jonka he saivat heti, kun soittivat Translate() He voivat:
await se, jos he haluavat estääIsCompleted kyselyynIsFaulted katsoa, epäonnistuiko seResult Käännettyjen sisältöjen saamiseksiKun näytämme tehtäviä käyttäjälle, projisoimme live-tehtävätilan näkymämalliksi:
// 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 on joitakin kummallisia tilayhdistelmiä:
IsCompleted on totta molemmat Onnistunut valmistuminen JA vialliset tehtävätIsCompletedSuccessfully pitää paikkansa vain menestyksestäIsFaulted tarkoittaa, että se heitti poikkeuksenIsCanceled tarkoittaa, että se peruttiinJoten sinun täytyy tarkistaa IsFaulted ennen tarkastusta IsCompleted, tai kohtelet epäonnistumisia menestyksinä.
Näkemyskyselyt päivityksiin HTMX:n avulla 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>
Näppärä kohta: hx-trigger="@trigger" tilaan perustuvat muutokset:
"none")Tämä on itsesääteleviä kannatuskyselyitä Se pysähtyy automaattisesti, kun ei ole mitään katsottavaa.
Kun käyttäjä napsauttaa "View" -painiketta, haemme valmiin käännöksen:
// 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 kansoittaa editorin:
// 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);
});
}
Koska:
Tämä on arkkitehtuuria Toivon, että selaimissa ja etukäteiskehyksissä olisi käytetty istuntotoimintoja.
Mitä saa ajattelematta liikaa:
Hyödy miten? |---------|-----| | Vakaus Rajattu valuutanvaihto kanavan ja semaforisen kuvion kautta | Negatiivisen palautteen silmukka Lisää kuormitusta → vastapainetta → rajoitettua muistia → johdonmukaista käytöstä | Vianetsintänäkyvyys Tarkat toiminnot ovat läsnä vain sen verran kauan, että ne voidaan tarkastaa. | Takaisinperittävyys Täydennetyt tulokset ovat saatavilla, kunnes ne päättyvät. | Nollatietojen säilyttäminen Yksityisyys ja yksinkertaisuus ovat kerrankin linjassa | Itsekeskeinen teloitus Vanhat tehtävät vähenevät, merkitykselliset jäävät
Aivan kuten LRU:n välimuisti teroitti käyttäytymismuistia, pyörivä leikkausikkuna terävöittää suoritusnäkymää.
Käytä sitä, kun:
Älä käytä sitä, kun:
Jos LRU:n artikkeli oli noin oppiminen unohtamallaTässä on kyse siitä, että Haihduttamalla.
Järjestelmä muistaa juuri sen verran, että siitä voi olla apua - eikä muuta.
Se on:
Parasta on se, että osaat jo toteuttaa sen: TaskCompletionSource, a Channel, a IMemoryCache liukuvan vanhenemisen ja taustatyöntekijän kanssa.
Ampukaa, älkääkä unohtako.
Sisään **Osa 2: Uudelleenkäytettävän teloituskirjaston rakentaminen**Muutamme kuvion apuriksi:
EphemeralForEachAsync<T> - kuten Parallel.ForEachAsync mutta toiminnan jäljitykselläEphemeralWorkCoordinator<T> - pitkähkö työjonoAddEphemeralWorkCoordinator<TCoordinator> (kuten AddHttpClient)© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.