关注点三:可靠性
如果你确保你的 bot 有正确的 错误处理,基本就可以运行了。 所有可能发生的错误(失败的 API 调用、失败的网络请求、失败的数据库查询、失败的中间件,等等)都被捕获。
你应当确保总是去 await
所有的 Promise,或者如果你不想等待的事情,至少也要调用 catch
去捕获错误。 可以使用 lint 规则去确保你不会忘记这些。
优雅关闭
对于使用了长轮询的 bot,还有更多的事要去考虑。 当你打算在某个操作期间再次停止你的实例,你应该去考虑捕获 SIGTERM
和 SIGINT
事件,并调用 bot
(长轮询内置的)方法或者通过它的 处理 (grammY runner)来停止你的 bot。
简单的长轮询
import { Bot } from "grammy";
const bot = new Bot("");
// 当 Node.js 进程将要被终止时,停止你的 bot。
process.once("SIGINT", () => bot.stop());
process.once("SIGTERM", () => bot.stop());
await bot.start();
2
3
4
5
6
7
8
9
const { Bot } = require("grammy");
const bot = new Bot("");
// 当 Node.js 进程将要被终止时,停止你的 bot。
process.once("SIGINT", () => bot.stop());
process.once("SIGTERM", () => bot.stop());
await bot.start();
2
3
4
5
6
7
8
9
import { Bot } from "https://deno.land/x/grammy@v1.27.0/mod.ts";
const bot = new Bot("");
// 当 Deno 进程将要被终止时,停止你的 bot。
Deno.addSignalListener("SIGINT", () => bot.stop());
Deno.addSignalListener("SIGTERM", () => bot.stop());
await bot.start();
2
3
4
5
6
7
8
9
使用 grammY runner
import { Bot } from "grammy";
import { run } from "@grammyjs/runner";
const bot = new Bot("");
const runner = run(bot);
// 当 Node.js 进程将要被终止时,停止你的 bot。
const stopRunner = () => runner.isRunning() && runner.stop();
process.once("SIGINT", stopRunner);
process.once("SIGTERM", stopRunner);
2
3
4
5
6
7
8
9
10
11
const { Bot } = require("grammy");
const { run } = require("@grammyjs/runner");
const bot = new Bot("");
const runner = run(bot);
// 当 Node.js 进程将要被终止时,停止你的 bot。
const stopRunner = () => runner.isRunning() && runner.stop();
process.once("SIGINT", stopRunner);
process.once("SIGTERM", stopRunner);
2
3
4
5
6
7
8
9
10
11
import { Bot } from "https://deno.land/x/grammy@v1.27.0/mod.ts";
import { run } from "https://deno.land/x/grammy_runner@v2.0.3/mod.ts";
const bot = new Bot("");
const runner = run(bot);
// 当 Deno 进程将要被终止时,停止你的 bot。
const stopRunner = () => runner.isRunning() && runner.stop();
Deno.addSignalListener("SIGINT", stopRunner);
Deno.addSignalListener("SIGTERM", stopRunner);
2
3
4
5
6
7
8
9
10
11
这就是基本的对可靠性所做的东西,你的实例现在将 ®️ 永远 ™️ 不会崩溃了。
可靠性保证
如果你的 bot 正在处理金融交易,你必须考虑一个 kill
的场景设想,即 CPU 物理故障或者数据中心断电该怎么办? 如果因为一些原因,某人或者某事真的很难处理这过程,它将会变得更加复杂。
本质上,bot 不能保证你的中间件只执行一次。 阅读一下 GitHub 上的这个 讨论 去了解更多 为什么 你的 bot 在某些极端情况下会重复发送信息(或者根本不发送)。 本章剩下的部分主要是详细解释 grammY 在这些不常见的情况下会怎样表现,并且怎样去处理这些情况。
如果你只关心怎样去编写一个 Telegram bot 的代码?跳过本章剩下的部分。
Webhook
如果你在 webhooks 模式下运行你的 bot,如果你的 bot 没有及时返回正确响应 Bot API 服务器将会再次尝试传送 updates 到你的 bot。 这基本上全面定义了系统的行为,如果你需要阻止处理重复的 updates,你应该基于 update
来构建你自己的重复数据删除。 grammY 没有为你做这些工作,但是如果你认为其他人可以从中获得收益,你可以向我们提交 PR 。
长轮询
长轮询是更加有意思的。 内置的轮询基本上是重新运行已获取但无法完成的最近的 update 批处理。
注意如果你使用
bot
正确停止了你的 bot ,Telegram 服务器会用正确的 偏移量 调用.stop get
,update 偏移量将会被同步但是不会处理 update 的数据。Updates
换句话说,你将不会错过任何的 update,不过,你可能会重新处理多达100个以前见过的 update。 由于对 send
的调用不是幂等的,用户可能会从你的 bot 收到重复的消息。 不过,至少有一次 处理是可以被保证的。
grammY Runner
如果你在并发模式使用 grammY runner, 下一次的 get
调用可能会在你的中间件处理当前批处理的第一个 update 之前执行。 因此,update 偏移量被提前 确认 。 这是高并发性的代价,不幸的是,如果不降低吞吐量和响应能力,就无法避免这种代价。 结果是,如果你的实例被正确的(或错误的)时间被关闭了,可能会发生多达 100 个 update 无法再次获取,因为 Telegram 认为它们已被确认。 这将会引起数据丢失。
如果防止这种情况非常重要,那么应该使用 grammY runner 库的沉(sink)和源(source)来组成自己的 update 管道,这个管道首先传递所有 update 到消息队列。
- 基本上来说,你必须创建一个发送到消息队列的 沉(sink),并启动一个只提供消息队列的运行程序。
- 然后,你必须再次创建一个从消息队列提取的 源(source) 。 你将有效的运行两个不同的 grammY runner 实例。
据我们所知,上述这个模糊的草案只是草图,还没有实现。 如果你有问题或者你想尝试并分享你的进展,请 联系 Telegram group 。
另一方面,如果你的 bot 出于高负载并且 update 轮循由于 自动加载限制 而减慢,那么再次获取 update 的机会将会增加,这将导致再次重复发送消息。 因此,完全并发的代价是既不能保证 至少一次处理,也不能保证 最多一次处理。