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

<!--category-- ASP.NET, Architecture, CQRS, Systems Design, Async, Privacy-Safe Design -->
<datetime class="hidden">2025-12-12T12:00</datetime>

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

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

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

Це супутник моєї попередньої статті, **[Вивчення LRUs.](/blog/learning-lrus-when-capacity-makes-systems-better)**як обмежена пам'ять і ковзання стають механізмом виживання.

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

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

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

- **Частина 1 (ця стаття)**: Теорія, шаблон і приклад реального світу.
- **[Частина 2. Побудова пам'ятної пам'ятки про страту](/blog/ephemeral-execution-library)**: Повна реалізація інтеграції DI

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

[Зараз це у пакунку maculicid.ephemerals Nuget також більше, ніж 20 centericd.ephemerals patterns та 'atoms'](https://www.nuget.org/packages?q=mostlylucid&includeComputedFrameworks=true&prerel=true&sortby=created-desc).

[![NuGet](https://img.shields.io/nuget/v/mostlylucid.ephemeral.svg)](https://www.nuget.org/packages/mostlylucid.ephemeral)
[![Ліцензія](https://img.shields.io/badge/license-Unlicense-blue.svg)](../../UNLICENSE)

[TOC]

---


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

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

### Fire- and- forget

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

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

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

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

Тепер у вас є:

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

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

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

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

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

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

---


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

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

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

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

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

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

```mermaid
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>`:

```csharp
// 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](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1) - подібна до обіцянки конструкція, яка дозволяє нам негайно повернутися, поки робота відбувається на задньому плані.

```csharp
// 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:

```javascript
// 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 негайно повертається з ідентифікатором завдання:

```csharp
// 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](/blog/learning-lrus-when-capacity-makes-systems-better) застосувати тут також:

> **Тримайте достатньо історії, щоб знешкодити і пояснити причину системи... і дозволити природному руйнуванню знищити все інше.**

```csharp
// 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 годин (абсолютної) або однієї години бездіяльності (хлипання)

```mermaid
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`:

```csharp
// 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:

```csharp
// 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` отримати перекладений зміст

---


## Проекція стану - Отримування стану зі стану завдання

Під час показу завдань користувачеві ми проектуємо стан реальних завдань у модель перегляду:

```csharp
// 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`:

```html
@* 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"`)

Це **саморегуляційне опитування** - Вона автоматично зупиняється, коли нема на що дивитися.

---


## Отримання результату

Після натискання користувачем " Перегляд " буде отримано завершений переклад:

```csharp
// 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 заповнить редактор:

```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](/blog/learning-lrus-when-capacity-makes-systems-better) було близько **навчання, забуваючи**, ось ця про **виконанням випаровування**.

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

Це:

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

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

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

---


## Що далі?

Вхід **[Частина 2. Побудова пам'ятної пам'ятки про страту](/blog/ephemeral-execution-library)**, ми перетворимо цей шаблон на помічника входження до системи:

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

---


## Посилання

- [Вивчення LRUs.](/blog/learning-lrus-when-capacity-makes-systems-better) - супутня стаття на об' єднаній пам' яті
- [Частина 2. Побудова пам'ятної пам'ятки про страту](/blog/ephemeral-execution-library) - повна реалізація
- [Документація з набору завданьComment](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1) - Досьє Майкрополя на TCS шаблоні
- [System. Threading.Channels](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels) - Примітивний виробник, який ми використовуємо
- [Документація IMemoryCache](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory) Вбудований кеш - ASP. NET