Back to "火 不要火 不要火 不错 - 忘记无国家即时处决"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

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

火 不要火 不要火 不错 - 忘记无国家即时处决

Friday, 12 December 2025

多数合成系统要么记住太多(logs, chenes, standing junky you never wanted)... 要么它们什么都不记得(火和忘记的黑洞一旦出事就会消失) 。

本条提出一些不同的说法:

一种模式, 每一个同步操作都变成一个微小的 可追踪, 可检查, 可再回收, 自我清洁任务单位 - 整个系统都是私密的 封闭的 快速的 决定性的

这是我前一篇文章的随从 **学习 LRU - 超能力何时使你的系统变得更好**该论文探讨了连接的内存+滑动失效如何成为生存机制的问题。

这个探索了另一半: 受约束,即时处决 - 活动任务的小滚动缓冲器会变成调试器、事件日志和重播引擎, 而不保留任何用户数据 。

这个例子来自我的博客的翻译部件 — 一个用于实时翻译标记内容的小界面。 它看起来微不足道。 在引擎盖下, 它可以执行您可以为任何同步工作流程窃取的模式 。

这是 第一部分 第一部分 由两部分组成的系列:

举起手来!

这现在存在于多数卢布的. 短期的Nuget包件中, 也存在于20多个多数卢布的. 短期的图案和“ 原子” 中。.

N Nuget 元数 许可证许可证许可证许可证


问题 - 同步工作流程创建隐藏状态

ASP.

灭火和忘记

你,你,你 Task.Run() 没有关联,没有重建,不知道什么失败了。

着火等待

堵住你不应该堵住的线条。 吞吐量会死, 静脉会爆炸。 您的用户讨厌你 。

各地的队列/通道

现在你有:

  • 无约束的队列
  • 坚持的状态,你不是故意要保持的
  • PII 意外存放在某处
  • 一个分布式系统,您只要一个简单的操作

分布追踪跟踪

有用,但外部。此外,它不能重放任何东西, 并且经常泄漏数据 你从未打算存储。

我们真正想要的是:

  • 短寿命
  • 私立
  • 可调试
  • 可检查
  • 受约束
  • 必要时可回收
  • 然后又离开,

"只是足够长的"记忆 系统在做什么 - 没有更多了。


模式 -- -- 火和 还用用不着 忘记

一句话里的想法是这样的:

立即取消一个自动同步操作, 明确通过任务完成源来跟踪它, 明确清除它, 并对最后几项任务保持微小的滚动缓冲, 以便您可以检查或检索它们- 而不保留任何用户内容 。

它坐在中间的中间:

  • 外部承包
  • 追踪追踪
  • 求职队列
  • 期货/期货
  • 预时会议

...不成为其中任何一个。

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

时间任务解剖

核心数据结构 TranslateTask -一个小的原子执行单位 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);

它包括:

  • 独一的 ID( 在整个流程中相交)
  • 时间戳( 开始时间)
  • 元数据( 语言 - 非用户内容 ! )
  • 提及实际 Task<TaskCompletion>
  • 持续时间(按访问计算)
  • 错误状态( 源自任务状态)
  • 最后结果(只有成功完成后)

什么都没有 什么都没有写在磁盘上 没有什么存储在滚动窗口之外

AWS有步骤功能,Azure有持久功能,你有...


任务完成源模式 - 将火力和火力转向火力和火力

魔法发生在 BackgroundTranslateService我们不用火和忘记,而是用 任务完成源 - 一种承诺式的构思 让我们在工作在幕后进行时 立即返回

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

什么是任务完成源?

如果你不使用 TaskCompletionSource<T> 之前,把它当做 承诺手动控制。 不同于经常 Task 当工作完成时完成, TCS 完成时完成 您 您 的 您 您 调 调 调 调 SetResult(), SetException(),或 SetCanceled().

这让你:

  1. 返回a Task 立即通知打电话者
  2. 在别的地方做实际工作(不同的线条、背景服务等)
  3. 于下列时间完成任务时完成任务 您 您 的 您 您 决定就做决定

这是"火与火"和"火与铁轨"之间的桥梁


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

响应是即时的。 实际翻译在背景中运行。 用户可以进行民意调查或只看 UI 更新 。


滚动操作窗口 - A 自我清除缓冲

关键的观点 LRR 条款 这里也适用:

保持足够的历史 来调试和解释这个系统... 让自然衰败抹去一切

// 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小时(绝对)或1小时无活动(滑动)后完全蒸发
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( 只需任务标识、 时间戳和语言代码 ) 。 没有 GDPR 头痛 。


环形货币货币-总督圈

背景服务处实施 固定货币 使用频道阅读器和 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");
        }
    }
}

这就像一个 驾驶蒸汽引擎的州长更多的负载 背压 建立自然振动 稳定。

该模式给您提供 :

  • 无超过螺旋螺旋 -不能产生无限的任务
  • 软实时行为 - 带宽的延迟时间
  • 连续暴发的自然平滑 - 通道缓冲尖尖
  • 稳定在装载中 - 优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优优

完成任务完成源

当实际翻译工作完成后,我们完成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 网络 Task 有一些怪异的状态组合:

  • IsCompleted 是真实的 两者兼 成功完成和错误的任务
  • 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、 时间戳、 语言)
  • 仅在任务结果中存在内容 - 这是在记忆中,附属于任务
  • 窗口被捆绑 - 每个用户最多5个任务,最多保留6小时
  • 万物自然腐烂 - 流出到期日清理非活动用户
  • 任何东西都无法触动磁盘 -没有日志,没有队列,没有数据库
  • 您不能意外保留 PII -它无处可去

这就是建筑结构 我希望会话操作使用浏览器和前端框架.


新兴行为

你得到什么 而不思考太辛苦:

福利如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 福利 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 的福利 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何 如何

--------- -----
负反馈回路 更多的负载 背压 捆绑的内存 一致的行为
调试可见度 准确的操作在现场 只要足够长的时间来检查
可再利用性 完成的结果在到期前可以查阅
零数据保留率 隐私和简洁 统一了一次
自优化执行 旧任务一去不复返,

就像路运联盟的缓冲器 放大了行为记忆一样 滚动操作窗口也放大了行刑视图


何时使用此模式

使用时:

  • 用户数据不得持续
  • 操作时间短(秒至分钟)
  • 调试事项
  • 隐私事务
  • 负载稳定性事项
  • 您想要即时响应+背景处理

不要在以下时间使用它:

  • 您需要真实的耐久性( 使用真实的队列)
  • 操作需要数小时(使用 点火或类似)
  • 您需要完全的语义( 使用分布式交易)
  • 多个服务器需要协调(使用 Redis 或信件中介)

结论 - 作为设计哲学的即期处决

如果 LRR 条款 大约 遗忘后学习,这一个是关于 通过蒸发执行.

系统记忆犹新,

这是:

  • 受约束
  • 确定性
  • 私立
  • 可调试
  • 可回收
  • 自我清洗
  • 强强
  • 和架构- 第一次

最好的事情?你已经知道如何执行它: TaskCompletionSource, a , a , a , a Channel, a, a IMemoryCache 与滑动到期, 和背景工人。

火... 别忘了


下一个是什么?

内 **第2部分:建立一个可重复使用的短执行图书馆**我们会把这个模式变成一个助推员

  • EphemeralForEachAsync<T> - 喜欢 Parallel.ForEachAsync 具有操作跟踪功能
  • 每实体相继执行的关键输油管
  • EphemeralWorkCoordinator<T> - 长寿命可观察的工作队列
  • 命名/类型协调员 AddEphemeralWorkCoordinator<TCoordinator> (类似) AddHttpClient)
  • 与范围寿命和单吨寿命寿命完全的DI集成
  • 与其他方法(TPL数据流、通道、背景服务)的比较

链接链接链接链接

logo

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