如何追踪 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() 注册位置,继续追踪具体回调。
3. 在 Cookie 调用处记录响应头状态
现有调用栈能回答谁调用了 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 当作已经修复;跳过后会话是否仍能正确工作,需要结合所用组件判断。
定位完成后,移除临时日志和源码改动,避免持续记录大量调用栈。
备注:内容仅供参考。
