PAYATHON 2026

如何追踪 WordPress Action Scheduler 任务并定位 Cookie 警告?

数据小陈

可以通过 Action Scheduler 的执行钩子记录任务 ID 和 hook,再将这些记录与 setcookie() 处的调用栈对应起来。还要检查响应头从哪里开始发送:设置 Cookie 的位置和导致响应头提前发送的位置可能不同。

目前的线索只能说明警告与 cron 执行有关。action_scheduler_run_queue 每分钟运行,并不能证明队列中存在一个每分钟执行的具体业务任务,也不能仅凭时间间隔确定责任插件。

1. 先确认修改的是实际加载的源码

多个插件可能同时携带 Action Scheduler。加载时,它会从已注册的副本中选择版本,因此修改 WooCommerce 目录下的文件,不一定会影响当前请求使用的实现。加载机制见 Action Scheduler 加载说明。

可以将以下代码临时放入调试插件,在产生警告的运行环境中记录方法所在的文件:

add_action('init', function () {
    if (!class_exists('ActionScheduler_Action')) {
        return;
    }

    $method = new ReflectionMethod('ActionScheduler_Action', 'execute');
    error_log('[AS source] ' . $method->getFileName());
}, 99);

如果记录的路径与修改位置不同,这就是日志没有生效的一种可能原因。如果路径一致,再检查警告是否发生在任务执行之前,以及日志是否写入预期位置;仍有疑问时,再检查 OPcache 的更新设置。

原始片段中的 $this-\>get_args() 如果只是复制时产生的转义,可以忽略;如果实际 PHP 文件中也是这样,必须改为 $this->get_args()。

2. 用执行钩子记录任务,避免继续修改依赖源码

当前官方执行器提供 action_scheduler_before_execute、action_scheduler_after_execute 和 action_scheduler_failed_execution 等钩子,分别用于观察执行前、回调返回后和执行失败的情况。具体调用位置见 队列执行器源码。站点未提供版本号,部署前应确认实际加载的版本包含这些钩子。

下面的临时调试代码记录进程 ID、任务 ID 和 hook,并保存当前任务 ID,方便与 Cookie 日志关联:

<?php
// 放入临时调试插件;不要添加 PHP 结束标签。

function faq_as_trace_log($phase, $action_id) {
    error_log(sprintf(
        '[AS pid=%s id=%s] %s',
        getmypid(),
        $action_id,
        $phase
    ));
}

add_action('action_scheduler_before_execute', function ($action_id) {
    $GLOBALS['faq_as_action_id'] = $action_id;
    $action = ActionScheduler::store()->fetch_action($action_id);

    faq_as_trace_log(
        'before hook=' . $action->get_hook(),
        $action_id
    );
}, 1, 1);

add_action('action_scheduler_after_execute', function ($action_id) {
    faq_as_trace_log('after', $action_id);
    unset($GLOBALS['faq_as_action_id']);
}, PHP_INT_MAX, 1);

add_action('action_scheduler_failed_execution', function ($action_id, $exception) {
    faq_as_trace_log('failed: ' . $exception->getMessage(), $action_id);
    unset($GLOBALS['faq_as_action_id']);
}, PHP_INT_MAX, 2);

add_action('action_scheduler_execution_ignored', function ($action_id) {
    faq_as_trace_log('ignored', $action_id);
    unset($GLOBALS['faq_as_action_id']);
}, PHP_INT_MAX, 1);

before 表示执行器准备处理该任务,之后仍可能因状态检查而跳过;after 表示任务回调已经返回。普通 PHP warning 不一定会变成异常,因此触发 Cookie 警告的任务仍可能出现 after,也可能在管理界面显示为完成。

复现后,按相同的进程 ID、任务 ID 和时间顺序检查日志。拿到 hook 名称后,在插件和主题代码中搜索该字符串及其 add_action() 注册位置,继续追踪具体回调。

现有调用栈能回答谁调用了 setcookie(),但不能直接回答响应头为什么已经发送。在原来的 setcookie() 前临时加入以下代码:

$sent_file = '';
$sent_line = 0;
$sent = headers_sent($sent_file, $sent_line);

error_log(sprintf(
    '[cookie pid=%s action_id=%s] headers_sent=%s output=%s:%d',
    getmypid(),
    isset($GLOBALS['faq_as_action_id'])
        ? $GLOBALS['faq_as_action_id']
        : 'unknown',
    $sent ? 'yes' : 'no',
    $sent_file,
    $sent_line
));

error_log(print_r(
    debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 24),
    true
));

当响应头已经发送时,headers_sent($sent_file, $sent_line) 可以提供输出开始的文件和行号;某些启动阶段输出的文件名可能为空。具体行为见 PHP headers_sent 手册。

将这两组证据分开看:

  • 调用栈指出哪个插件、回调或会话初始化过程尝试设置 Cookie。
  • headers_sent() 的结果指出输出从哪里开始,需要结合该请求的执行流程判断原因。

如果 action_id 为 unknown,先检查调用栈中是否真的进入了任务回调。警告也可能出现在 cron 请求加载插件、初始化会话或准备运行队列的阶段,此时对单个队列任务的追踪还没有覆盖到出错位置。

这里使用 DEBUG_BACKTRACE_IGNORE_ARGS,是为了避免把 Cookie、会话数据等参数直接写入日志。

4. 用管理界面对照任务记录

安装 WooCommerce 的站点可以进入 WooCommerce → 状态 → 计划操作(Scheduled Actions),按记录到的 hook、任务 ID 和时间查找任务,查看其状态及执行日志。这个页面支持查看和调试计划任务,入口见 Action Scheduler 管理界面说明。

管理界面适合确认具体任务是否运行;完整定位 PHP warning 仍需要前面的错误日志和调用栈。如果要手动运行任务复现,先确认任务会执行什么操作,优先在测试环境进行。

5. 根据调用路径修复

找到责任代码后,再决定修复方式:

  • 如果是调试输出、文件中的意外空白或显示到响应中的 PHP 错误导致提前输出,修复对应的输出源。
  • 如果后台任务初始化了面向访客的会话,并尝试写入浏览器 Cookie,检查该任务是否需要这一步,以及会话组件是否提供适合后台执行的用法。
  • 如果调用发生在响应已经结束或刷新之后,调整会话初始化或 Cookie 设置的时机。

当前材料还不足以确定哪一种情况成立。不要把 @setcookie()、全局输出缓冲或简单跳过设置 Cookie 当作已经修复;跳过后会话是否仍能正确工作,需要结合所用组件判断。

定位完成后,移除临时日志和源码改动,避免持续记录大量调用栈。

备注:内容仅供参考。