Zsens Admin 插件定时任务踩雷:当 `wp_schedule_event` 的"静默失败"让我凌晨三点收到二十封重复邮件

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
插件开发 48 浏览 0 回复

上周上线了一个自动清理过期日志的功能,本地测试一切正常,结果生产环境凌晨开始疯狂发邮件。查日志发现同一个任务被注册了十几遍,每分钟都在跑。复盘之后发现是 wp_schedule_event 的"幂等性陷阱"——这货不会自动去重,重复调用就重复注册,而且失败时连句报错都没有。

先贴我最初的错误写法,估计不少人踩过:

// ❌ 错误:每次插件激活/页面加载都直接注册,无任何防护
function zsens_register_cleanup_hook() {
    wp_schedule_event(time(), 'hourly', 'zsens_log_cleanup');
}
add_action('wp', 'zsens_register_cleanup_hook');

// 回调里发通知邮件
function zsens_log_cleanup() {
    $deleted = zsens_purge_old_logs();
    wp_mail(get_option('admin_email'), '日志清理完成', "删除了 {$deleted} 条记录");
}
add_action('zsens_log_cleanup', 'zsens_log_cleanup');

问题一:wp_schedule_event 不会检查是否已存在同名任务,每次执行都新建一个 cron 条目。我插件里又在 wp 钩子上触发的,页面访问量一上来,任务表直接爆炸。

问题二:没有返回值校验。wp_schedule_event 失败时返回 falseWP_Error,但代码完全没处理,连日志都没打。

问题三:时间戳用了 time(),导致每次注册的实际触发点都不一样,任务队列里东一个西一个,根本没法追踪。

修正后的写法,核心就三句话:先查再注、固定锚点、失败即落盘

// ✅ 正确:带幂等校验与错误处理的注册逻辑
function zsens_register_cleanup_hook() {
    $hook = 'zsens_log_cleanup';
    $next = wp_next_scheduled($hook);
    
    // 已存在则直接跳过,绝不重复注册
    if ($next !== false) {
        return;
    }
    
    // 用整点锚定,方便排查与对齐业务周期
    $anchor = strtotime(date('Y-m-d H:00:00'));
    $result = wp_schedule_event($anchor, 'hourly', $hook);
    
    if ($result === false || is_wp_error($result)) {
        // 落盘到自有日志,别依赖 WP_Cron 的静默
        zsens_log_error('cron_register_failed', [
            'hook'   => $hook,
            'anchor' => $anchor,
            'result' => $result,
        ]);
    }
}

// 只在特定场景触发注册,绝不用 wp 这种高频钩子
register_activation_hook(ZSENS_PLUGIN_FILE, 'zsens_register_cleanup_hook');

// 卸载时显式清理,防止残留幽灵任务
register_deactivation_hook(ZSENS_PLUGIN_FILE, function () {
    $hook = 'zsens_log_cleanup';
    $timestamp = wp_next_scheduled($hook);
    while ($timestamp) {
        wp_unschedule_event($timestamp, $hook);
        $timestamp = wp_next_scheduled($hook);
    }
});

回调侧也做了防御,防止任务重叠执行(服务器卡顿时 WP-Cron 可能并发触发):

// ✅ 带执行锁的回调
function zsens_log_cleanup() {
    $lock_key = 'zsens_cleanup_running';
    $locked = get_transient($lock_key);
    
    if ($locked !== false) {
        zsens_log_error('cron_overlap_detected', ['time' => time()]);
        return; // 已有实例在跑,直接放弃
    }
    
    set_transient($lock_key, time(), 300); // 5分钟锁
    
    try {
        $deleted = zsens_purge_old_logs();
        // 邮件改到每天汇总发,别每次清理都轰炸
        if (date('H') === '02') {
            wp_mail(get_option('admin_email'), '日志清理日报', "本日删除 {$deleted} 条");
        }
    } finally {
        delete_transient($lock_key); // 确保锁释放
    }
}

几个血泪细节:

1. wp_next_scheduled 的返回值

成功返回时间戳(int),未找到返回 false。注意不是 0 不是 null,必须用 === false 判断,否则 1970 年的时间戳会被误判。

2. 锚定时间戳的选择

别用 time()strtotime('+1 hour'),每次计算结果不同,任务表里的 time 字段会乱成一锅粥。固定整点/整分,排查时一眼能对上。

3. 卸载清理的 while 循环

如果已经炸了、同一个 hook 有多个条目,wp_clear_scheduled_hook 在部分 WP 版本里有漏删的 bug(老版本只删第一个匹配项)。手动 while + wp_next_scheduled + wp_unschedule_event 更稳妥。

4. 别在 wp / init 里注册 cron

这些钩子每次请求都跑,访问量大的站点瞬间给你造出几百个任务。只在激活时注册一次,或者挂到 admin_init 并加页面条件判断。

最后补个快速诊断命令,进数据库直接看:

SELECT * FROM wp_options WHERE option_name = 'cron' 
AND option_value LIKE '%zsens_log_cleanup%';

如果看到同一个 hook 出现多次,就是重复注册了。也可以装 WP Crontrol 插件图形化排查,但生产环境建议直接查表,避免装额外插件。

你们 cron 任务还踩过哪些静默坑?比如 spawn_cron 返回 0 但实际没执行、或者服务器时区跟 WP 时区对不上导致触发时间漂移的,欢迎丢案例。

评论0
回复 · 0
还没有回复
微信客服 微信客服