Вогонь! Досить. Забуття. (Українська (Ukrainian))

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

Friday, 12 December 2025

//

14 minute read

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

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

Шаблон, у якому кожна асинхронна дія стає крихітною можна відстежити, Можна переглядати, 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 з масштабованим та однотонним життям
  • Порівняння з іншими підходами (ТПП, канали, служби тла)

Посилання

Finding related posts...
logo

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