# Eld och Don't *Ganska* Glöm - Öfmjukelse utan stat

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

De flesta async-system kommer antingen ihåg för mycket (logs, köer , kvarlevda skräp du aldrig ville ha, )… eller så kommer de inte ihåg någonting alls. | ( | eld | - | och - | glöm svarta hål som försvinner när något går fel

Denna artikel introducerar något annorlunda

> Ett mönster där varje async operation blir en liten **spårbar**, **kontrollerbart**, **re-avtagbar**, **själv-tvättande arbetsenhet** - och hela systemet stannar privatt , begränsat, , snabbt,

' är min tidigare artikels medföljare. **[Att lära sig LRUs - När överskott gör ditt system bättre](/blog/learning-lrus-when-capacity-makes-systems-better)**. Att man utforskade hur ett begränsat minne + glittrande utplåning blir en överlevnadsmekanism

Den här utforskar den andra halvan **bounded, tidiga straff** - hur en liten rollande buffer av aktiva uppgifter blir en debugger , en händelselog, , och en återspelningsmaskin

Det exemplet kommer från min blogg's översättningsguidan | - | en liten gränssnitt för att översätta markerdown innehåll på |- | the |- | fly | . | Det ser triviellt ut |

Det här är **Del 1** av en serie med två delar

- **Del 1 (den här artikelnM SK2**: Teorin, ,, mönstret och ett verkligt exempel på världen
- **[Del 2: Att bygga en återanvändbar ephemeral Execution Library](/blog/ephemeral-execution-library)**: Den fulla implementeringen med DI-integration

## NUGET!!!

Detta är nu även i det mest uppenbara packaget Nuget [mer än 20](https://www.nuget.org/packages?q=mostlylucid.ephemeral&includeComputedFrameworks=true&prerel=true&sortby=created-desc).

[![NuGet](https://img.shields.io/nuget/v/mostlylucid.ephemeral.svg)](https://www.nuget.org/packages/mostlylucid.ephemeral)
[![Licens](https://img.shields.io/badge/license-Unlicense-blue.svg)](../../UNLICENSE)

[TOC]

---


## källdates

Den fulla källkoden finns i [mostlylucid.atoms GitHubs arkiv](https://github.com/scottgal/mostlylucid.atoms)

## Problemet -Async arbetsflöden skapa dolda tillstånd

ASP.NET-utvecklare tenderar att välja mellan tre dåliga alternativ

### "Fire" - och - "forget

Du `Task.Run()` något , hoppas att det slutar, och när det exploderar får du ingenting . Ingen korrelation M SK3 ingen rekonstruering MSC4 ingen aning om vad som misslyckades

### "Fire" - - - och - - vänta

Du blockerar trådar du inte borde blockera

### köer, /, kanaler överallt

Nu har du

- obegränsade köer
- ihärdlig tillstånd du inte tänkt att behålla
- PII lagrats oavsiktligt någonstans
- ett distribuerat system där man bara ville ha en enkel operation

### Distribuerad spårning

användbar… men externt. också : den kan inte spela upp någonting igen och läcker ofta data som du aldrig hade tänkt att lagra

Vad vi egentligen vill är

- **kort-liv**
- **hemliga**
- **lösbar**
- **kontrollerbart**
- **begränsat**
- **möjlig att hämta om det behövs**
- **och sedan var den borta**

En """, bara tillräckligt lång, """, minne av vad systemet gör.

---


## Pattern -Fire och *Don't Klart* Glöm

Här är idén i en mening.

> **Starta en async operation omedelbart, spåra den eksplicitt via en TaskCompletionSource**

Det ligger halvvägs mellan

- eventsourcing
- att spåra
- jobb köer
- framtida löften
- tidiga sammankomster

… utan att bli någon av dem

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

---


## Anatomi av en tidig uppgift

Kerndatastrukturen är `TranslateTask` - en liten atomisk uträttelseenhet som stöds av `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);
```

Det innehåller:

- En unik ID ( korrelerar över hela strömmen )
- En tidsramp ( när den börjadeM SK1
- Metadata (språk
- En referens till den faktiska `Task<TaskCompletion>`
- Tydlighet (beräknat när man får tillgång till den
- Błędstaten (odlat från uppgiftsstaten)
- Slutresultat ( endast om färdigt framgångsrikt

Det ', det är det , ., inget överlevt, ., inget skrivet på disken, ., inget lagret bortom en rullande fönstertil

AWS har stegfunktioner. Azure har hållbara funktioner. Du har ♫ … ♫ ♫ 30 ♫ rader kod som inte kräver en molnbil ♫' ♫

---


## TaskCompletionSource Pattern -Störning av eld - och-Ska bort att elda

Magien händer i `BackgroundTranslateService`Istället för eld använder vi [Uppbyggnadskällan](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1) - ett löfte - som en konstruktion som låter oss återvända omedelbart medan arbetet sker i bakgrunden

```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;
}
```

### Vad är en TaskCompletionSource , "?"?

Om du inte använt `TaskCompletionSource<T>` före "," tänkte man på det som en **lova att du kontrollerar det manuell**. Till skillnad från en vanlig `Task` som slutar när dess arbete slutar, en TCS slutar *du* ringa `SetResult()`, `SetException()`, eller `SetCanceled()`.

Det här låter dig

1. Ge tillbaka en `Task` till telefonören omedelbart
2. Gör det faktiska arbetet någon annanstans (skiljbar tråd , bakgrundstjänsterM SK2 etc.)
3. Gör uppgiften när *du* bestämma att "'" är gjort

Det är bryggan mellan ", brand, - och -, glömt " och ", brand.

---


## API-layret -Starta en översättning

När en user klickar "Verta", så ropar frontend på 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: n kommer tillbaka omedelbart med bara en uppgift ID:

```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);
}
```

Svaret är omedelbart. Den faktiska översättningen körs i bakgrunden

---


## Rolling Operationsfenster -A Self-Cleaning Buffer

Den viktigaste insikten från [LRU-artikel](/blog/learning-lrus-when-capacity-makes-systems-better) gäller även här:

> **Håll bara tillräckligt med historia för att avbugera och resonera kring systemet… och låt naturlig förruttning radera allt annat.**

```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; }
    }
}
```

Varje ny uppgift:

- Är tilldana till user's rullande venster
- Springer ut den äldsta om vi
- Evaporeras helt efter 6 timmar (absolutt | ) | eller ♫ 1 | en timme av inaktivitet

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

Inget förvarande risk. Ingen PII i kaset ( bara uppgiftersidentifiseringar , tidstämningar M SK3 och språkkoder

---


## Bounded Concurrency -The Governor Loop

Hintergrundtjänsterna implementerar **fikst samtida partners** loop med hjälp av en kanalreader `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");
        }
    }
}
```

Det här är som en **gaskraftverksledare**: mer belastning → baktryck byggs ♫ ♫ → ♫ naturlig drivkraft ♫

Patternet ger dig

- **Inga överlastspiraler** -kan't skapa oändliga uppgifter
- **Soft real-tidsbeteende** -begränsad latens
- **Naturligt glättande av sprängningar** -kanal bufferar spetsen
- **Stabilitet under last** - bryter sig elegant

---


## Completing of the TaskCompletionSource

När den faktiska översättningsarbeiten slutar

```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);
    }
}
```

Kallaren's `Task<TaskCompletion>` - som de fick direkt när de ringde `Translate()` - slutar nu . De kan

- `await` om de vill blockera
- Kontroller `IsCompleted` att rösta
- Kontroller `IsFaulted` för att se om det misslyckades
- Kontroller `Result` för att få översatt innehåll

---


## Statusprognosen -Deriving State from Task State

När vi visar upp uppgifter för användaren, projicerar vi den verkliga uppgifternas tillstånd i en modell

```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;
    }
}
```

### En anteckning om arbetslägen

.NET's `Task` har några märkliga tillståndskombinationer:

- `IsCompleted` är sant för **båda** framgångsrikt utförande och felaktiga uppgifter
- `IsCompletedSuccessfully` är bara sant för framgång
- `IsFaulted` innebär att det kastade en undantag
- `IsCanceled` betyder att den har avgått

Så du måste kolla `IsFaulted` före att kolla `IsCompleted`, eller så ska du treatera misslyckanden som framgångar

---


## Ui -HTMX Polling

Synundersökningar för uppdaterade med HTMX's `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>
```

Den smarta biten: `hx-trigger="@trigger"` förändringar baserade på tillstånd:

- Om uppgifterna fortfarande pågår
- Om alla uppgifter är utförda: sluta rösta (`"none"`)

Det här är **självregulerande val** - stannar automatiskt när det finns något att titta på

---


## Att hämta utfallet

När användaren klickar "View", får vi slutgiltiga översättningen

```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 befolkar sedan redaktören:

```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);
        });
}
```

---


## Varför är det här privat?

Eftersom:

- **Inget användarinnehåll lagras i kaset** - endast uppgifter för uppgifter
- **innehållet existerar bara i uppgifternas resultat** - som är i minnet, som ansluts till uppgifterna
- **Oknet är begränsat** -max 5 uppgifter per användare
- **Allt bryts ner naturligt** -sliding expiration cleans up inactive users
- **Inget rör vid disket** - inga logs , inga köer M SK2 ingen databas
- **Du kan inte oavsiktligt behålla PII** -, där, ', det finns inget ställe dit den ska gå

Det här är arkitekturen *Jag önskar att webblader och frontend ramverk hade använts för sessioner*.

---


## Det uppväxande beteendet

Vad du får utan att tänka för hårt:

| nytta | Hur |
|---------|-----|
| **Stabilitet** | Begränsad samtida genom kanal + mönster av mönsters |
| **Negativ återkopplingsloop** | Mer belastning | → | baktryck |→ | begränsad minne | МSK3 | konsekventa beteende |
| **Debug-visibilitet** | De exakta operationerna är närvarande tillräckligt länge för att inspektera
| **Lösningsbarhet** | Completed results are accessible until they expire |
| **Noll databevarande** | Privatsphäre och enkelhet sammanlänkade för en gång |
| **Self-optimering av uitvoering** | Gamla uppgifter faller bort

Precis som LRU-キャッシュen hade täckt beteendemässiga minnen, täcker rollandets operationsfenster inspelningsbilden.

---


## När man ska använda det här mönstret

Använd den när:

- Användardata måste inte finnas kvar
- Operationer är korta
- Problemet löser
- Privatlivsfrågor
- Ladestabilitet
- Du vill ha en omedelbar respons + bakgrundsprocess

Använd inte den när

- Du behöver riktig hållbarhet (använd en riktig kö)
- Operationer tar timmar (använda Hangfire eller liknande )
- Du behöver exakt-en gång semantik | ( | använda en fördelad transaktion
- Flera server behöver koordinera (använda Redis eller ett meddelandesmakare)

---


## slutsats -Permanent utföring som en designfilosofi

Om [LRU-artikel](/blog/learning-lrus-when-capacity-makes-systems-better) var **att lära sig genom att glömma**, den här handlar om **utförd genom att avdunsta**.

Systemet kommer ihåg precis tillräckligt mycket för att vara hjälpsamt - och inget mer .

Det's:

- begränsat
- deterministisk
- hemliga
- lösbar
- lösbar
- själv-tvättande
- robust
- och arkitektur-first

Det bästa? Du vet redan hur du ska implementera det `TaskCompletionSource`, a `Channel`, an `IMemoryCache` med skiftig utgångsdatum , och en bakgrundsarbetare.

**"Fire" … och "Don"**

---


## Vad's Nästa?

I **[Del 2: Att bygga en återanvändbar ephemeral Execution Library](/blog/ephemeral-execution-library)**, vi ' ska göra detta mönster till en droppe

- `EphemeralForEachAsync<T>` - som `Parallel.ForEachAsync` men med operationsspårning
- Keyed pipelines for per-entity sequential execution
- `EphemeralWorkCoordinator<T>` - en lång
- Namerade koordinatorer med "/". `AddEphemeralWorkCoordinator<TCoordinator>` ( som `AddHttpClient`)
- Full DI integration med skalad och enton livslängd
- jämförelse med andra tillvägagångssätt (TPL Dataflow

---


## Links

- [Att lära sig LRUs - När överskott gör ditt system bättre](/blog/learning-lrus-when-capacity-makes-systems-better) -kommande artikel om begränsad minne
- [Del 2: Att bygga en återanvändbar ephemeral Execution Library](/blog/ephemeral-execution-library) -fulla implementering
- [Dokumentation för uppfyllda uppgifter](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1) -Microsoft's dokumentationer på TCS-mönstret
- [System.Threading.Channel](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels) -tillverkaren-konsument primitiv vi använder
- [IMemoryCache-dokumentation](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory) -ASP