Tuli ja älä Aikamoista Unohtakaa teloitus ilman valtiota (Suomi (Finnish))

Tuli ja älä Aikamoista Unohtakaa teloitus ilman valtiota

Friday, 12 December 2025

//

13 minute read

Useimmat asynkkijärjestelmät joko muistavat liikaa (lokit, jonot, sinnikkäät roskat, joita et koskaan halunnut)... tai he eivät muista yhtään mitään (tulta ja unohda mustia aukkoja, jotka katoavat, kun jokin menee pieleen).

Tässä artikkelissa esitellään jotain erilaista:

Kaava, jossa jokaisesta asynkistä operaatiosta tulee pieni Jäljitettävä, Tarkastettu, Uudelleen noudettavissa, itsepuhdistava tehtäväyksikkö Koko järjestelmä pysyy yksityisenä, rajattuna, nopeana ja deterministisenä.

Se on aiemman artikkelini kumppani. **LRU:iden oppiminen - Kun suurempi kapasiteetti tekee järjestelmästäsi paremman**Tuo tutki, kuinka rajatusta muistista ja liukuvasta vanhenemisesta tulee selviytymismekanismi.

Tämä tutkii toista puolikasta: Rajoitettu, ohimenevä teloitus − Miten pieni liikkuva puskuri aktiivisista tehtävistä muuttuu vianetsintä-, tapahtumaloki- ja uusintamoottoriksi ilman, että käyttäjätietoja säilytetään.

Esimerkki on blogini käännöswidgetistä, joka on pieni käyttöliittymä maaliviivan sisällön kääntämiseen lentoon. Se näyttää vähäpätöiseltä. Huppujen alla se toteuttaa kuvioita, joita voi varastaa mihin tahansa asynkkiseen työnkulkuun.

Tämä on Osa 1 kaksiosaisesta sarjasta:

NUGET!!!

Tämä on nyt enimmäkseen lucid.efemerals Nuget-paketissa myös yli 20 enimmäkseen lucid.efemerals-mallia ja 'atoms'.

NuGet Lisenssi


Ongelma - Async Workflows Luo piilotilaa

ASP.NET-kehittäjät valitsevat yleensä kolmesta huonosta vaihtoehdosta:

Tuli ja unohdus

Sinä Task.Run() Toivottavasti se loppuu, ja kun se räjähtää, ei saa mitään.

Paloa ja odottelua

Latenssi räjähtää ja käyttäjät vihaavat sinua.

Jonot/kanavat kaikkialla

Nyt sinulla on:

  • rajattomia jonoja
  • sinnikäs tila, jota ei ollut tarkoitus pitää
  • PII tallentui vahingossa jonnekin
  • jaettu järjestelmä, jossa halusit vain yksinkertaisen toiminnan

Hajautettu jäljitys

Hyödyllistä, mutta ulkoista. Se ei myöskään pysty toistamaan mitään ja vuotaa usein tietoja, joita ei ole koskaan tarkoitettu tallentamaan.

Itse asiassa haluamme, että:

  • lyhytaikainen
  • yksityinen
  • debuggattava
  • Tarkastettu
  • rajatut
  • noudettavissa tarvittaessa
  • ja sitten poissa

"Vielä tarpeeksi pitkä" muisto siitä, mitä järjestelmä tekee, eikä mitään muuta.


Kaava - tuli ja Älä aivan Unohda

Yhdessä lauseessa on ajatus:

Aloita async-operaatio välittömästi, seuraa sitä nimenomaan TaskCompletionSourcen kautta, puhdista se deterministisesti ja pidä pieni pyörimispuskuri viimeisissä tehtävissä, jotta voit tarkastaa tai noutaa ne – säilyttämättä mitään käyttäjäsisältöä.

Se istuu puolivälissä:

  • tapahtumahankinnat
  • jäljitys
  • työjonot
  • Futuurit/lupaukset
  • Äärimmäiset istunnot

...tulematta keneksikään heistä.

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

Efeerisen tehtävän anatomia

Perusdatarakenne on TranslateTask -pieni atomin teloitusyksikkö, jonka tukena on 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);

Se sisältää:

  • Yksilöllinen tunniste (korjautuu koko virtaukseen)
  • Aikaleima (kun se alkoi)
  • Metadata (kieli - ei käyttäjäsisältöä!)
  • Viittaus todelliseen Task<TaskCompletion>
  • Kesto (laskettu pääsyllä)
  • Virhetila (johtuu tehtävätilasta)
  • Lopputulos (vain jos suoritettu onnistuneesti)

Mikään ei kestänyt, ei ollut kirjoitettua levylle, ei ollut tallennettuna pyörivän ikkunan taakse.

AWS:llä on Step Functions. Azurella on kestäviä toimintoja. Sinulla on 30 riviä koodia, jotka eivät vaadi pilvilaskua.


TaskComplectionSource -malli - Tulipalojen ja sammutustöiden unohtaminen

Taikuus tapahtuu BackgroundTranslateServicePalon ja unohduksen sijaan käytämme Tehtäväntäyttölähde − Luvatun kaltainen rakennelma, jonka avulla voimme palata välittömästi, kun työt tapahtuvat taustalla.

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

Mikä on TaskCompletionLähde?

Jos et ole käyttänyt TaskCompletionSource<T> ennen, ajattele sitä lupaa hallita manuaalisestiToisin kuin tavallinen Task joka valmistuu, kun sen työ päättyy, TCS valmistuu, kun sinä soitto SetResult(), SetException(), tai SetCanceled().

Näin voit:

  1. Palauta a Task soittajalle heti
  2. Tee varsinainen työ jossain muualla (erilainen lanka, taustapalvelu jne.)
  3. Suorita tehtävä loppuun, kun sinä Päätä, että se on tehty.

Se on "tulen ja unohduksen" ja "tulen ja raidan" välinen silta.


API-taso aloittaa käännöksen

Kun käyttäjä napsauttaa "käännä", etuosa kutsuu 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 palaa välittömästi pelkällä tehtävätunnisteella:

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

Vastaus on instant. Varsinainen käännös kulkee taustalla. Käyttäjä voi tehdä kyselyjä tai vain katsoa UI-päivityksen.


Rolling Operation Window - Itsepuhdistava puskuri

Avainnäkemys: LRU:n artikkeli Koskee myös tätä asiaa:

Pidä vain sen verran historiaa, että voit selvittää järjestelmän ja antaa luonnollisen rappeutumisen pyyhkiä pois kaiken muun.

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

Jokainen uusi tehtävä:

  • On lisätty käyttäjän vierintäikkunaan
  • Työntää ulos vanhimman, jos olemme valmiudessa (5 max)
  • Haihduttaa kokonaan kuuden tunnin (absoluuttinen) tai yhden tunnin (liukumaton) jälkeen
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

Ei retentioriskiä. Ei PII:tä välimuistissa (vain tehtävätunnisteet, aikaleimat ja kielikoodit). Ei GDPR-päänsärkyä.


Rajattu valuutta - Kuvernööri Loop

Taustapalvelu toteuttaa kiinteä vaihtovelkakirja silmukka käyttäen kanavanlukijaa ja 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");
        }
    }
}

Tämä on kuin kuvernööri höyrymoottorilla: lisää kuormaa → vastapainetta rakentaa → luonnollista thratting → vakautta.

Kaava antaa sinulle:

  • Ei ylikuormituskierrettä -ei voi synnyttää rajattomia tehtäviä
  • Pehmeä reaaliaikainen käytös -sidottu latenssi
  • Murtumien luonnollinen tasaus - Kanava puskee piikit
  • Vakaus kuormitettuna - halventaa sulavasti

Tehtäväntäytäntöönpanolähde

Kun varsinainen käännöstyö päättyy, suoritamme TCS:n:

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

Soittajan Task<TaskCompletion> - Jonka he saivat heti, kun soittivat Translate() He voivat:

  • await se, jos he haluavat estää
  • Tarkista IsCompleted kyselyyn
  • Tarkista IsFaulted katsoa, epäonnistuiko se
  • Tarkista Result Käännettyjen sisältöjen saamiseksi

Statusprojektori - Deriving State from Task State

Kun näytämme tehtäviä käyttäjälle, projisoimme live-tehtävätilan näkymämalliksi:

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

Työvaltiota koskeva huomautus

.NET's Task on joitakin kummallisia tilayhdistelmiä:

  • IsCompleted on totta molemmat Onnistunut valmistuminen JA vialliset tehtävät
  • IsCompletedSuccessfully pitää paikkansa vain menestyksestä
  • IsFaulted tarkoittaa, että se heitti poikkeuksen
  • IsCanceled tarkoittaa, että se peruttiin

Joten sinun täytyy tarkistaa IsFaulted ennen tarkastusta IsCompleted, tai kohtelet epäonnistumisia menestyksinä.


UI-HTMX:n pollaus

Näkemyskyselyt päivityksiin HTMX:n avulla 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>

Näppärä kohta: hx-trigger="@trigger" tilaan perustuvat muutokset:

  • Jos tehtävät ovat yhä kesken: kysely viiden sekunnin välein
  • Jos kaikki tehtävät on tehty: äänestysten lopettaminen ("none")

Tämä on itsesääteleviä kannatuskyselyitä Se pysähtyy automaattisesti, kun ei ole mitään katsottavaa.


Tulosten hakeminen

Kun käyttäjä napsauttaa "View" -painiketta, haemme valmiin käännöksen:

// 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 kansoittaa editorin:

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

Miksi tämä on yksityisyyden suojaa

Koska:

  • Välimuistiin ei tallenneta käyttäjäsisältöä -vain tehtävän metatiedot (henkilötunnus, aikaleima, kieli)
  • Tehtävän tuloksessa on vain sisältöä -joka on muistissa, mukana tehtävässä
  • Ikkuna on rajattu -Maksimi 5 tehtävää käyttäjää kohti, enimmäiskesto 6 tuntia
  • Kaikki lahoaa luonnostaan - liukuva vanheneminen puhdistaa toimettomia käyttäjiä
  • Mikään ei kosketa levyä -ei lokitietoja, ei jonoja, ei tietokantaa
  • PII:tä ei voi vahingossa säilyttää - Ei ole paikkaa minne mennä.

Tämä on arkkitehtuuria Toivon, että selaimissa ja etukäteiskehyksissä olisi käytetty istuntotoimintoja.


Tuleva käytös

Mitä saa ajattelematta liikaa:

Hyödy miten? |---------|-----| | Vakaus Rajattu valuutanvaihto kanavan ja semaforisen kuvion kautta | Negatiivisen palautteen silmukka Lisää kuormitusta → vastapainetta → rajoitettua muistia → johdonmukaista käytöstä | Vianetsintänäkyvyys Tarkat toiminnot ovat läsnä vain sen verran kauan, että ne voidaan tarkastaa. | Takaisinperittävyys Täydennetyt tulokset ovat saatavilla, kunnes ne päättyvät. | Nollatietojen säilyttäminen Yksityisyys ja yksinkertaisuus ovat kerrankin linjassa | Itsekeskeinen teloitus Vanhat tehtävät vähenevät, merkitykselliset jäävät

Aivan kuten LRU:n välimuisti teroitti käyttäytymismuistia, pyörivä leikkausikkuna terävöittää suoritusnäkymää.


Milloin tätä kaavaa käytetään

Käytä sitä, kun:

  • Käyttäjätiedot eivät saa säilyä
  • Toiminta on lyhytkestoista (sekunneista minuutteihin)
  • Vianetsintäasiat
  • Yksityisyysasiat
  • Kuormanvakavuudella on merkitystä
  • Haluat pikavastauksen + taustakäsittelyn

Älä käytä sitä, kun:

  • Tarvitset todellista kestävyyttä (käytä oikeaa jonoa)
  • Operaatiot kestävät tunteja (käytä Hangfirea tai vastaavaa)
  • Tarvitset tasan kerran semantiikkaa (käytä jaettua tapahtumaa)
  • Useita palvelimia on koordinoitava (käytä Redisiä tai viestinvälittäjää)

Johtopäätös - Efeerinen teloitus suunnittelufilosofiana

Jos LRU:n artikkeli oli noin oppiminen unohtamallaTässä on kyse siitä, että Haihduttamalla.

Järjestelmä muistaa juuri sen verran, että siitä voi olla apua - eikä muuta.

Se on:

  • rajatut
  • deterministinen
  • yksityinen
  • debuggattava
  • noudettavissa
  • itsepuhdistus
  • järeä
  • Arkkitehtuuri ensin

Parasta on se, että osaat jo toteuttaa sen: TaskCompletionSource, a Channel, a IMemoryCache liukuvan vanhenemisen ja taustatyöntekijän kanssa.

Ampukaa, älkääkä unohtako.


Mitä seuraavaksi?

Sisään **Osa 2: Uudelleenkäytettävän teloituskirjaston rakentaminen**Muutamme kuvion apuriksi:

  • EphemeralForEachAsync<T> - kuten Parallel.ForEachAsync mutta toiminnan jäljityksellä
  • Avatut putkistot per yksikkö peräkkäistä toteutusta varten
  • EphemeralWorkCoordinator<T> - pitkähkö työjono
  • Nimetyt/kirjoitetut koordinaattorit AddEphemeralWorkCoordinator<TCoordinator> (kuten AddHttpClient)
  • Täydellinen DI-integraatio laajakantoisten ja yksitoikkoisten elinimien kanssa
  • Vertailu muihin lähestymistapoihin (TPL Dataflow, kanavat, taustapalvelut)

Linkkejä

Finding related posts...
logo

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