Back to "Вогонь! Досить. Забуття."

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

Architecture ASP.NET Async CQRS Privacy-Safe Design Systems Design

Вогонь! Досить. Забуття.

Friday, 12 December 2025

Більшість асинхронних систем або пам'ятають забагато (логів, чергів, брухту, якої ви ніколи не хотіли)... або вони зовсім нічого не пам'ятають (вогне-забуваючи чорні діри, які зникають в момент, коли щось іде не так).

У цій статті подається щось інше:

Шаблон, у якому кожна асинхронна дія стає крихітною можна відстежити, Можна переглядати, re- retrievable, Самоочищення модуля задачі -а вся система залишається приватною, обмеженою, швидкою і детермінованою.

Це супутник моєї попередньої статті, **Вивчення LRUs.**як обмежена пам'ять і ковзання стають механізмом виживання.

Ось цей досліджує іншу половину: пов' язане, ефемеральне виконання - Як мініатюрний буфер згортання активних завдань стане зневадником, журналом подій і рушієм повтору, без збереження даних користувача.

Приклад можна знайти у віджеті перекладу мого блогу - невеличкому інтерфейсі для перекладу вмісту markdown на ходу. Цей віджет виглядає тривіальним. Під каптуром він реалізує шаблони, які ви можете вкрасти для будь- якого робочого процесу.

Це Частина 1 серії з двома частинами:

ГОЛОСНО!!!!!

Зараз це у пакунку maculicid.ephemerals Nuget також більше, ніж 20 centericd.ephemerals patterns та 'atoms'.

NuGet Ліцензія


Проблема - синхронний потік даних створює схований стан

Розробники ASP. NET зазвичай вибирають один з трьох варіантів:

Fire- and- forget

Ви Task.Run() ні зв'язку, ні реконструкції, ні поняття що не вдалося.

Пожежа і очікування

Ви блокуєте нитки, які вам не варто блокувати.

По черзі/ Канали всюди

Тепер у вас є:

  • необ' єднані черги
  • постійний стан, який ви не хотіли залишити
  • Неналежно збережено десь pIIQLiteResult
  • розподілена система, у якій ви просто бажали простої дії

Розподілити слідкування

Корисна... але зовнішня. Крім того, вона не може відтворювати будь- які дані і часто пропускає дані, які ви ніколи не мали намір зберігати.

Насправді ми хочемо:

  • коротке життя
  • конфіденційний
  • debuggable
  • Можна переглядати
  • обмежено
  • Можна повторити, якщо потрібно
  • а потім зникне

"Стільки довга" пам'ять про те, що робить система - і нічого більше.


Зразок вогню і Не робіть цього! Забути

В одному реченні ідея:

Негайно вимкніть асинхронізацію, відстежте її явно за допомогою пункту меню Завершення завдань, зачистіть її детерміновано, і тримайте крихітний буфер згортання останніх декількох завдань так, щоб ви могли переглядати або отримувати їх - без збереження будь-якого вмісту користувача.

Він сидить посередині між:

  • адміністрація події
  • слідкування
  • черги завдань
  • майбутні/ обіцянки
  • ephemeral сеанси

...не перетворюючись на жодного з них.

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 рядків коду, які не потребують купюри хмар.


Шаблон набору завдань - перехід до вогню-і-запуску до Fire- and-Track

Магія відбувається в 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;
}

Що таке "CompletionSource"?

Якщо ви ще не використовували TaskCompletionSource<T> раніше, думати про це як пообіцяти, що керуватимете вручну. На відміну від звичайного Task закінчує роботу TCS після завершення роботи, коли ви виклик SetResult(), SetException(), або SetCanceled().

Це дозволяє вам:

  1. Повернути a Task негайно перед викликом
  2. Виконати роботу в іншому місці (іншу гілку, фонову службу тощо)
  3. Закінчити завдання, якщо@ info: whatsthis ви вирішити, що це зроблено

Це міст між "Вогонь-вогню" і "Вогню-доріжком."


Шар API - запуск перекладу

Після натискання користувачем " 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);
}

Відповідь є миттєвою. Сам переклад працює у фоновому режимі. Користувач може проводити опитування або просто спостерігати за оновленням інтерфейсу користувача.


Вікно згортання операцій - A само- очищення буферу

Головне розуміння Стаття 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; }
    }
}

Кожне нове завдання:

  • Додається до вікна згортання користувача
  • Виштовхує найстаріше, якщо ми в об'ємі (5 макс)
  • Випаровується повністю після 6 годин (абсолютної) або однієї години бездіяльності (хлипання)
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");
        }
    }
}

Це схоже на Губернатор парового двигуна.: Більше навантаження → Тиск спини будує → природний пульсуючий → стабільність.

Взірець дає вам:

  • Без перевантаження спіраль - не вдалося створити необмежені завдання
  • Поведінка у режимі реального часу - переобтяжена спізнення
  • Природне згладжування вибухів - Каналні буфери використовують шипи
  • Стабільність під навантаженням - Природоохоронно.

Завершення завданняCompletionSource

Коли переклад завершився, ми завершили 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Або ви вважатимете невдачі успіхами.


Опитування UI - HTMX

Опитування для оновлень за допомогою 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" зміни, засновані на стані:

  • Якщо завдання все ще виконуються: опитування кожні 5 секунд
  • Якщо всі завдання буде виконано: припинити опитування ("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);
        });
}

Чому це безпеки приватності

Тому що:

  • Вмісту користувача не збережено у кеші - лише метадані завдання (ID, часовий штамп, мова)
  • Вміст існує лише у результаті завдання - який у пам' яті, з' єднано з завданням
  • Вікно обмежено - max 5 завдань для користувача, max 6 hours reconstriction
  • Все само руйнується. - застаріле вичищує неактивних користувачів
  • Нічого не торкнеться диска - без журналів, без черг, без бази даних
  • Ви не можете випадково зберегти PII - Тут нема куди йти

Це архітектура Я хотів би, щоб навігатори і оболонки оболонки використовувалися для операцій сеансів.


Зміцняюча сила

Те, що ти отримаєш, не подумавши занадто сильно:

ДІАПА |---------|-----| | Стабільність ♪ | Цикл від' ємного зворотнього зв' язку ♪ Більше завантажуйте → тиск назад → Прив'язана пам'ять → послідовна | Видимість зневаджування ♫ Точні процедури наявні лише так довго, що можна сказати: | Можливість отримання Дівчата доступні до тих пір, поки вони не зупиняться на результатах. | Зберігання нульових даних і простота, які вишикувались за один раз | Самооптимізація виконання ♫ Старі завдання відпадають, доречні залишаються

Подібно як кеш LRU загострив поведінку пам' яті, циклічне вікно операції загострює перегляд програми.


Коли слід використовувати цей приклад

Використовувати його, якщо:

  • Дані користувача не повинні зберігатися
  • Дії короткочасні (у секундах до хвилин)
  • Зневадження справ
  • Приватна сфера має значення
  • Завантажити стабільність має значення
  • Вам потрібна миттєва відповідь + фонова обробка

Не використовуйте її, коли:

  • Вам потрібна справжня стійкість (скористайтесь справжньою чергою)
  • Операції потребують годин (використовувати поновлення вогню або подібні дії) Noun, a list of items
  • Вам потрібні лише один раз семантики (використовуйте розподілену операцію)
  • Для координат потрібно декілька серверів (використовуйте Redis або брокер повідомлень)

Заключення - пророча страта як планова філософія

Якщо Стаття LRU було близько навчання, забуваючи, ось ця про виконанням випаровування.

Система запам'ятовує достатньо, щоб бути корисною і не більше.

Це:

  • обмежено
  • детермінований
  • конфіденційний
  • debuggable
  • Можна повторити
  • самоочищення
  • стійкий
  • і архітектура- перша

Найкраще? ти вже знаєш, як його реалізувати: TaskCompletionSource, a Channel, an IMemoryCache с проскользком и фирменным работником.

Вогонь... і не зовсім забудь.


Що далі?

Вхід Частина 2. Побудова пам'ятної пам'ятки про страту, ми перетворимо цей шаблон на помічника входження до системи:

  • EphemeralForEachAsync<T> - наприклад Parallel.ForEachAsync але з стеженням за операцією
  • трубопроводи з ключами для послідовного виконання
  • EphemeralWorkCoordinator<T> - довгострокову робочу чергу, яку можна бачити
  • Іменовані/ Типовані координатори з AddEphemeralWorkCoordinator<TCoordinator> (на зразок AddHttpClient)
  • Повна інтеграція DI з масштабованим та однотонним життям
  • Порівняння з іншими підходами (ТПП, канали, служби тла)

Посилання

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.