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
Більшість асинхронних систем або пам'ятають забагато (логів, чергів, брухту, якої ви ніколи не хотіли)... або вони зовсім нічого не пам'ятають (вогне-забуваючи чорні діри, які зникають в момент, коли щось іде не так).
У цій статті подається щось інше:
Шаблон, у якому кожна асинхронна дія стає крихітною можна відстежити, Можна переглядати, re- retrievable, Самоочищення модуля задачі -а вся система залишається приватною, обмеженою, швидкою і детермінованою.
Це супутник моєї попередньої статті, **Вивчення LRUs.**як обмежена пам'ять і ковзання стають механізмом виживання.
Ось цей досліджує іншу половину: пов' язане, ефемеральне виконання - Як мініатюрний буфер згортання активних завдань стане зневадником, журналом подій і рушієм повтору, без збереження даних користувача.
Приклад можна знайти у віджеті перекладу мого блогу - невеличкому інтерфейсі для перекладу вмісту markdown на ходу. Цей віджет виглядає тривіальним. Під каптуром він реалізує шаблони, які ви можете вкрасти для будь- якого робочого процесу.
Це Частина 1 серії з двома частинами:
Розробники ASP. NET зазвичай вибирають один з трьох варіантів:
Ви Task.Run() ні зв'язку, ні реконструкції, ні поняття що не вдалося.
Ви блокуєте нитки, які вам не варто блокувати.
Тепер у вас є:
Корисна... але зовнішня. Крім того, вона не може відтворювати будь- які дані і часто пропускає дані, які ви ніколи не мали намір зберігати.
Насправді ми хочемо:
"Стільки довга" пам'ять про те, що робить система - і нічого більше.
В одному реченні ідея:
Негайно вимкніть асинхронізацію, відстежте її явно за допомогою пункту меню Завершення завдань, зачистіть її детерміновано, і тримайте крихітний буфер згортання останніх декількох завдань так, щоб ви могли переглядати або отримувати їх - без збереження будь-якого вмісту користувача.
Він сидить посередині між:
...не перетворюючись на жодного з них.
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
Структура даних core є структурою даних TranslateTask - крихітна атомна одиниця виконання, що підтримується a 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);
У ньому є:
Task<TaskCompletion>Нічого не залишилося, нічого написаного на диск, нічого, що зберігається за вікном.
У AWS є крокові функції. У Azure є придатні функції. У вас є... 30 рядків коду, які не потребують купюри хмар.
Магія відбувається в BackgroundTranslateServiceЗамість того, щоб розпалювати вогонь, ми використовуємо Завершення завданьComment - подібна до обіцянки конструкція, яка дозволяє нам негайно повернутися, поки робота відбувається на задньому плані.
// 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;
}
Якщо ви ще не використовували TaskCompletionSource<T> раніше, думати про це як пообіцяти, що керуватимете вручну. На відміну від звичайного Task закінчує роботу TCS після завершення роботи, коли ви виклик SetResult(), SetException(), або SetCanceled().
Це дозволяє вам:
Task негайно перед викликомЦе міст між "Вогонь-вогню" і "Вогню-доріжком."
Після натискання користувачем " Translate ," графічна оболонка викликає 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 негайно повертається з ідентифікатором завдання:
// 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);
}
Відповідь є миттєвою. Сам переклад працює у фоновому режимі. Користувач може проводити опитування або просто спостерігати за оновленням інтерфейсу користувача.
Головне розуміння Стаття LRU застосувати тут також:
Тримайте достатньо історії, щоб знешкодити і пояснити причину системи... і дозволити природному руйнуванню знищити все інше.
// 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; }
}
}
Кожне нове завдання:
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
Без ризику повторного збереження. Немає PII у кеші (лише ідентифікатори задач, часові штампи і коди мови). Від головного болю від ВВП.
Служба тла реалізує a фіксована узгодженість цикл за допомогою засобу для читання каналів і 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");
}
}
}
Це схоже на Губернатор парового двигуна.: Більше навантаження → Тиск спини будує → природний пульсуючий → стабільність.
Взірець дає вам:
Коли переклад завершився, ми завершили 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);
}
}
Викликальника Task<TaskCompletion> - яку вони отримали негайно, коли вони зателефонували Translate() Вони можуть:
await якщо вони хочуть блокуватиIsCompleted для опитуванняIsFaulted переконатися, що спроба виконання дії зазнала невдачіResult отримати перекладений змістПід час показу завдань користувачеві ми проектуємо стан реальних завдань у модель перегляду:
// 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 має декілька незвичайних комбінацій станів:
IsCompleted true для обидва Успішне завершення або помилкові завданняIsCompletedSuccessfully встановлено лише для успіхуIsFaulted означає, що викинув винятокIsCanceled означає, що його було скасованоТак что вы должны проверить IsFaulted перед перевіркою IsCompletedАбо ви вважатимете невдачі успіхами.
Опитування для оновлень за допомогою 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>
Розумний момент: hx-trigger="@trigger" зміни, засновані на стані:
"none")Це саморегуляційне опитування - Вона автоматично зупиняється, коли нема на що дивитися.
Після натискання користувачем " Перегляд " буде отримано завершений переклад:
// 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 заповнить редактор:
// 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);
});
}
Тому що:
Це архітектура Я хотів би, щоб навігатори і оболонки оболонки використовувалися для операцій сеансів.
Те, що ти отримаєш, не подумавши занадто сильно:
ДІАПА |---------|-----| | Стабільність ♪ | Цикл від' ємного зворотнього зв' язку ♪ Більше завантажуйте → тиск назад → Прив'язана пам'ять → послідовна | Видимість зневаджування ♫ Точні процедури наявні лише так довго, що можна сказати: | Можливість отримання Дівчата доступні до тих пір, поки вони не зупиняться на результатах. | Зберігання нульових даних і простота, які вишикувались за один раз | Самооптимізація виконання ♫ Старі завдання відпадають, доречні залишаються
Подібно як кеш LRU загострив поведінку пам' яті, циклічне вікно операції загострює перегляд програми.
Використовувати його, якщо:
Не використовуйте її, коли:
Якщо Стаття LRU було близько навчання, забуваючи, ось ця про виконанням випаровування.
Система запам'ятовує достатньо, щоб бути корисною і не більше.
Це:
Найкраще? ти вже знаєш, як його реалізувати: TaskCompletionSource, a Channel, an IMemoryCache с проскользком и фирменным работником.
Вогонь... і не зовсім забудь.
Вхід Частина 2. Побудова пам'ятної пам'ятки про страту, ми перетворимо цей шаблон на помічника входження до системи:
EphemeralForEachAsync<T> - наприклад Parallel.ForEachAsync але з стеженням за операцієюEphemeralWorkCoordinator<T> - довгострокову робочу чергу, яку можна бачитиAddEphemeralWorkCoordinator<TCoordinator> (на зразок AddHttpClient)© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.