跳转到内容
use-cases / daily-digest-fan-out / hero
CRON · EXEC · PIPE FAN-OUT

一个定时摘要扇出到 200 个收件箱

每周一上午 9 点,一条 cron 条目唤醒一个容器。脚本把摘要渲染一次,写到一个带 ?n=200 的 pipe URL。两百个 curl 循环 —— 每个订阅者一个 —— 并行拉取同一份字节,交给 SMTP。扇出活在底层,不在你的代码里。

阅读 cron 文档
use-cases / daily-digest-fan-out / mechanism

Cron、exec、pipe —— 三次调用就完事

Hoody Cron API 在受管理条目里放下一行 5 字段 crontab。这一行运行一个 exec 脚本,把摘要渲染一次,然后推到一个带 n=200 的 pipe 路径。两百个订阅者循环并行拉取同一条路径 —— 服务器什么也不持有,慢的读者也无法阻塞其他人。

cron · entries
POST · schedule
# 周一 09:00 —— 受管理 cron 条目POST /users/root/entries# 发送到 /users/root/entries 的请求体{schedule: "0 9 * * 1",command: "bash /scripts/digest.sh"}
exec · digest.sh
PUT · sender
# 渲染一次 —— markdown → HTMLdigest=$(render-digest.py)# 把字节推到 pipe 路径上echo "$digest" | curl -T - https://pipe.hoody.com/api/v1/pipe/digest-monday?n=200# Pipe 阻塞直到 200 个接收者连上,然后流式发送
pipe · subscribers
GET · receivers
# 200 个轻量 curl 循环,每个订阅者一个while read addr; docurl -s https://pipe.hoody.com/api/v1/pipe/digest-monday?n=200 \| smtp-send "$addr" &done < subscribers.txt# 全部 200 并行流式发送 —— 反压处理慢的那些[INFO] Transfer complete.

cron 没有变得更复杂。扇出被搬进了底层 —— pipe 什么都不持有,脚本只渲染一次,循环只是边缘上的 SMTP。无队列、无重试表、无 campaign 工具席位。

use-cases / daily-digest-fan-out / powers

为什么 HTTP 扇出胜过 SMTP 扇出

朴素的设计串行循环 200 次 SMTP 发送,要 11 分钟,半路崩溃时还会重复投递。pipe 形态白送你并行性、幂等性和更小的容器。

PARALLELISM

两百个接收者,一次渲染

摘要恰好被构建一次。两百个 curl 循环同时拉取同样的字节。4 秒的运行替代了 11 分钟的串行循环 —— pipe 对慢读者施加反压,而不阻塞其他人。

IDEMPOTENCY

无中途崩溃要清理

没有 campaign 状态表要查。如果在 200 个全部连上之前运行就死了,pipe TTL 会清理未完成的那一半,下一次 cron 触发会重新渲染。无重复投递,无半发批次要对账。

ECONOMICS

一个容器,沉睡 23 小时

脚本一周醒一次,跑四秒,容器随后回到空闲。你为这四秒付费 —— 不是为一个一直在线的 campaign 服务,不是为按收件人计费的 SES,也不是为 Mailchimp 的席位。

use-cases / daily-digest-fan-out / timing

当电线本身做扇出,有什么变化

同样的 200 个收件人,同样的摘要主体。变化的是运行的形态 —— 从分钟级的串行 SMTP,变成秒级的并行 HTTP。

  1. RUN DURATION4.2s

    从 cron 触发到最后一次投递的墙钟时间。pipe 并行向所有 200 个接收者流式发送;瓶颈变成最慢订阅者的 SMTP,不是循环。

  2. RENDER COUNT

    摘要主体只计算一次。pipe 把同样的字节转发给每一个接收者 —— 没有按收件人重新渲染模板、没有按收件人计费、没有按收件人的缓存。

  3. RECEIVERS PER PATH200

    Hoody Pipe API 把 n 限制在 256。一份 200 人的周报舒舒服服地落在天花板之下 —— 慢读者会施加反压,但不阻塞其他人。

限制以 Hoody Pipe API 为准:接收者数 1–256,等待连接的 pipe TTL 5 分钟,服务器全局 1000 个活跃传输。cron 条目本身是 /users/root/entries 中的一行,带 schedule、command 和可选的 expires_at。

use-cases / daily-digest-fan-out / steps

周一上午 9 点,运行如何展开

四个时刻。每一个都是你本来要手敲的一次 HTTP 调用。Cron 是闹钟;exec 是渲染器;pipe 是电线;循环是 agent 唯一要写的东西。

    01
    09:00:00

    Cron 触发

    /users/root/entries 上的受管理条目触发。Schedule:0 9 * * 1。Command:bash /scripts/digest.sh。crontab 本身只是一条 JSON 记录 —— 不是 Airflow DAG,也不是工作流服务。

    02
    09:00:00

    渲染一次

    exec 脚本拉取本周数据、渲染 markdown、转成 HTML,把主体写到 stdout。一次渲染,一份 payload —— 没有按收件人的邮件合并循环。

    03
    09:00:00

    PUT pipe ?n=200

    脚本把 stdout 用管道交给 curl -T -,目标是 pipe/digest-monday?n=200。pipe 持有上传,直到 200 个接收者连上,然后并行向他们全部流式发送主体。

    04
    09:00:04

    200 次 SMTP

    两百个循环 curl 同一条路径,把主体交给各自订阅者的 SMTP。慢的那些拿到反压。快的那些在毫秒级完成。整个运行在数秒内结束。

use-cases / daily-digest-fan-out / punchline

一条 cron,一个容器,两百个收件人。

你以前的做法底层的做法
BEFORE · SERIAL SMTP WORKERfor sub in 200: smtp.send(render(sub))11 分钟 · 崩溃时半数已发 · 按收件人计费
AFTER · ONE PIPE PATHrender | curl -T - pipe/digest?n=2004 秒 · 幂等 · 一次唤醒的账单
阅读 pipe 规范
use-cases / daily-digest-fan-out / replaces

这取代了什么

想给一个名单发同一封邮件时常会去够的工具。每一个都向你收一份服务等级费,而归根结底就是一次渲染加一个扇出 HTTP 循环。

  • SendGrid scheduled campaigns为你脚本已经产出的 payload 按邮件计费
  • Mailchimp daily digests为每周一封,开整套 campaign UI 和一个受众席位
  • Custom mail-merge cron jobs一个串行循环、一个重试表,以及一份半发批次的事后报告
  • AWS SES + Lambda scheduled batch一个队列、一个 worker、一个 IAM 角色,加一个要照看的 CloudWatch 告警
  • Resend with batched API calls为发送之间没变过的主体按收件人花 API 钱
  • Customer.io drip campaigns为一个你本来就放在文本文件里的名单,开一个分群引擎
use-cases / daily-digest-fan-out / cta

周一 9 点曾意味着一个 worker 一封封 SMTP 慢慢磨。如今它只意味着一次 cron 触发、一个容器,以及一根 pipe 把剩下的事干完。

阅读 cron 指南
use-cases / daily-digest-fan-out / related

阅读其他内容