You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#在Heroku处理SIGTERM解决Telegram Bot实例冲突报错

问题根因
  • 报错Conflict: terminated by other getUpdates request; make sure that only one bot instance is running的核心原因是同一个Bot Token同时存在两个活跃的getUpdates长连接。Heroku在dyno部署、重启、休眠唤醒时,会先给旧实例发送SIGTERM信号,预留10秒优雅关闭窗口,窗口结束后直接发送SIGKILL强杀进程,同时启动新实例。如果旧实例在10秒窗口内没有主动断开getUpdates连接,新实例启动后就会和未完全退出的旧实例抢占连接,触发Telegram API的冲突报错。
  • 你当前的信号处理代码存在两个致命问题:
    1. 信号监听逻辑用Task.Factory.StartNew放到后台线程执行,属于fire-and-forget(发后不理)模式,信号触发后执行回调时,没有阻塞主进程等待清理完成,旧实例的Bot客户端还在持续发送getUpdates请求。
    2. 回调逻辑没有主动停止Bot轮询、等待正在处理的消息任务完成,旧连接没有被主动断开,完全靠Heroku的SIGKILL强制回收进程。
正确实现方案

核心思路是:收到SIGTERM信号后第一时间触发轮询取消,主动断开getUpdates连接,等待所有正在执行的业务逻辑跑完,在Heroku的10秒窗口内完成所有清理后再退出进程。

  • 第一步:重构信号监听类,同时兼容SIGTERM(容器终止信号)和SIGINT(本地调试Ctrl+C中断信号),暴露取消令牌用于绑定Bot轮询生命周期,不要用后台线程跑信号等待。
  • 第二步:将信号的取消令牌传入Bot的轮询启动方法,收到信号时自动触发轮询停止。
  • 第三步:信号触发后主动等待轮询任务终止,设置8秒超时(留2秒余量避免触发Heroku的强制杀进程逻辑),确认所有连接断开后再退出。

修正后的代码实现

1. 重构后的Unix信号处理类

using Mono.Unix;
using Mono.Unix.Native;

public class UnixExitSignal : IDisposable
{
    private readonly UnixSignal[] _trackedSignals = {
        new(Signum.SIGTERM),
        new(Signum.SIGINT)
    };
    private readonly CancellationTokenSource _exitCts = new();

    /// <summary>
    /// 进程退出触发的取消令牌,可绑定到Bot轮询、业务逻辑的生命周期
    /// </summary>
    public CancellationToken ExitToken => _exitCts.Token;

    /// <summary>
    /// 阻塞当前线程等待退出信号
    /// </summary>
    public void WaitForExit()
    {
        // 阻塞等待信号,不要放到后台线程执行
        UnixSignal.WaitAny(_trackedSignals, -1);
        // 收到信号后触发取消通知
        _exitCts.Cancel();
    }

    public void Dispose()
    {
        _exitCts.Dispose();
        foreach (var signal in _trackedSignals)
        {
            signal.Dispose();
        }
    }
}

2. 主程序接入示例(以官方Telegram.Bot库为例)

using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegram.Bot.Types;

// 初始化Bot客户端
var botClient = new TelegramBotClient("替换为你的Bot Token");
// 注册业务更新、错误处理器
var updateHandler = new DefaultUpdateHandler(HandleUpdateAsync, HandleErrorAsync);

// 初始化信号监听
using var exitSignal = new UnixExitSignal();
CancellationToken exitToken = exitSignal.ExitToken;

// 启动长轮询,绑定退出令牌
Task pollingTask = botClient.StartReceivingAsync(
    updateHandler,
    receiverOptions: new ReceiverOptions
    {
        // 按业务需求配置允许接收的更新类型
        AllowedUpdates = Array.Empty<UpdateType>()
    },
    cancellationToken: exitToken
);

// 阻塞主线程等待退出信号
exitSignal.WaitForExit();

try
{
    // 最多等待8秒让轮询停止、业务逻辑执行完成
    pollingTask.Wait(TimeSpan.FromSeconds(8));
}
catch (AggregateException ex) when (ex.InnerExceptions.All(e => e is OperationCanceledException))
{
    // 轮询取消属于预期行为,忽略该异常
}

// 到此所有getUpdates连接已主动断开,可安全退出
return;

// 业务更新处理逻辑
async Task HandleUpdateAsync(ITelegramBotClient client, Update update, CancellationToken ct)
{
    // 所有长耗时操作记得传入ct,收到退出信号可及时中断
    // 替换为你的业务代码
}

// 错误处理逻辑
Task HandleErrorAsync(ITelegramBotClient client, Exception exception, CancellationToken ct)
{
    // 替换为你的错误日志逻辑
    return Task.CompletedTask;
}
避坑说明
  • Heroku的优雅关闭窗口固定为10秒,所有清理、等待逻辑总耗时不能超过这个值,设置8秒超时是为了预留网络请求、资源回收的缓冲时间,避免进程被强制杀死。
  • 不要用Task.Run/Task.Factory.StartNew把信号等待逻辑放到后台线程,必须让主线程阻塞在信号等待逻辑上,否则.NET运行时可能在清理逻辑未执行完时就提前退出进程。
  • 所有业务逻辑中的长耗时操作(比如数据库读写、外部接口请求)都要传入绑定了退出信号的CancellationToken,收到信号后及时中断,避免拖慢关闭流程。
  • 同一Bot Token同一时间只能跑一个长轮询实例,不要在Heroku上配置多个dyno跑同一个Bot,否则必然触发409冲突。

内容的提问来源于stack exchange,提问作者Wheela Studio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 18:42:25