wp_schedule_event() で入れた予定は、ずっと残るとは限りません。消えたときに入れ直す処理が無いと、そのプラグインの定期処理は、誰にも気づかれずに止まります。WordPress 7.1.2 で、予定を消してからリクエストを1回送り、何が戻るかを測りました。
| 消し方 | その後のリクエスト | 本体の更新確認3つ | 本体のゴミ箱削除・期限切れ transient 削除 | 自作プラグイン 1.21.0 | 1.21.1 |
|---|---|---|---|---|---|
wp_clear_scheduled_hook() |
公開側1回 | (消していない) | (消していない) | なし | なし |
| 〃 | 管理側1回 | 〃 | 〃 | なし | 戻る(5分後) |
cron オプションごと削除 |
公開側1回 | 戻る | なし | なし | なし |
| 〃 | 管理側1回 | 戻る | (下記) | なし | 戻る |
本体の更新確認(wp_version_check など3つ)は、init で毎回確かめて入れ直しています。ゴミ箱の削除と期限切れ transient の削除は、wp-admin/admin.php で同じことをしています。プラグインの予定は、自分で同じことをしない限り戻りません。
先に断っておくと、register_activation_hook() で登録した処理がプラグインの更新では走らないことは、公式のドキュメントに書かれています。WordPress 本体が自分の予定を wp_next_scheduled() で確かめて入れ直していることも、ソースを読めば分かります。ここで書くのは、自作の WordPress プラグインで予定が消えたまま戻らない形になっていたことと、1.21.1 で足した入れ直しが、どの消え方で効くかを測った結果です。
何が起きていたか
自作の Rapls AI Chatbot には、サイトの記事を毎日読み込み直す raplsaich_crawl_site という予定があります。入れていたのは、有効化のときと、版が上がったときの2か所だけでした。
private static function schedule_cron() {
if (!wp_next_scheduled('raplsaich_crawl_site')) {
wp_schedule_event(time(), 'daily', 'raplsaich_crawl_site');
}
// ...
}
schedule_cron() は、有効化の activate() と、版の記録が古いときに毎リクエストで走る upgrade() から呼ばれます。更新で走らない問題は、こちらで済んでいました。抜けていたのは、同じ版のまま予定だけが消えた場合です。次に版が上がるまで、読み込みは止まったままになります。
1.21.1 で、init にこう足しました。
// The daily event is scheduled on activation only. When Site Learning
// is switched on and the event has gone missing (cron reset, a
// migration), schedule it again so learning resumes. Admin requests
// only, to keep the check off the front end.
if (is_admin() && !wp_next_scheduled('raplsaich_crawl_site')) {
$settings = get_option('raplsaich_settings', []);
if (!empty($settings['crawler_enabled'])) {
wp_schedule_event(time() + 300, 'daily', 'raplsaich_crawl_site');
}
}
コメントの「activation only」は正確ではなく、実際は有効化と版の更新の2か所です。
測り方
WordPress 7.1.2、PHP 8.4.21。プラグインを有効化したあと、予定を消す操作と、リクエスト1回を、それぞれ別の PHP プロセスで走らせました。リクエストは wp-load.php を読むだけで、管理側は WP_ADMIN を定義して is_admin() を真にしています。どちらも DISABLE_WP_CRON を定義して、リクエスト中に cron が走らないようにしました。
この管理側の再現では、init は走りますが、admin_init と wp-admin/admin.php は通りません。表の「本体のゴミ箱削除…」が管理側で空欄なのは、そのためです。本物の管理画面を開けば、admin.php が入れ直します。
消え方ごとの結果
| 操作 | 1.21.0 | 1.21.1 |
|---|---|---|
| 予定だけ消す → 公開側 | なし | なし |
| 予定だけ消す → 管理側 | なし | あり(+300秒) |
cron オプションを削除 → 管理側 |
なし | あり(+300秒) |
| 予定を消して、版の記録を古くする(更新と同じ状態)→ 公開側 | あり(+0秒) | あり |
| サイト学習をオフにしてから予定を消す → 管理側 | なし | なし |
| 〃 → そのあとオンにする → 管理側 | なし | あり(+300秒) |
| 無効化 → 有効化 | あり | あり |
オフのあいだは入れ直しません。オンにした次の管理画面で戻ります。版の更新で戻るのは、1.21.0 でも同じです。
同じプラグインの、古い会話を片付ける raplsaich_cleanup_old_conversations も、有効化と版の更新の2か所でしか入れていません。cron オプションを削除したあと、1.21.1 の管理側のリクエストでも戻りませんでした。こちらはまだ同じ形のままです。
毎回確かめる重さ
wp_next_scheduled() は、自動読み込みされた cron オプションの配列を見るだけです。1万回呼んで、1回あたり 5.68 マイクロ秒、追加のクエリは0本でした(予定が8件の環境)。WordPress 本体は、更新確認の3つを公開側も含めた毎回の init で確かめています。1.21.1 が管理側だけにしているのは、公開側に処理を足さないためで、重さの都合ではありません。
同時に入れ直しても、重ならないか
wp_schedule_event() は、繰り返しの予定について、同じフックがすでにあっても止めません。
wp_schedule_event(time() + 300, 'daily', 'zz_test'); // true
wp_schedule_event(time() + 301, 'daily', 'zz_test'); // true
wp_schedule_event(time() + 7200, 'daily', 'zz_test'); // true
// → 予定は3件
時刻が違えば、同じ予定が別に入ります。重ねないのは、呼ぶ側の wp_next_scheduled() の確認です。
では、予定が消えた状態で管理画面のリクエストが同時に来たらどうなるか。並べて投げて、残った件数を数えました。
| 投げ方 | 回数 | 残った予定 |
|---|---|---|
| 同時に2・4・8・16本 | 各3回 | すべて1件 |
| 0.15秒ずつずらして12本 | 3回 | すべて1件 |
| 0.03秒ずつずらして40本(1本0.24秒) | 5回 | すべて1件 |
重なりは出ませんでした。cron オプションは配列まるごとの上書きなので、あとから書いたプロセスの配列が残っているのだろうと見ていますが、そこは確かめていません。
予定を入れるとき
- 有効化のときだけ入れた予定は、版が同じあいだに消えると戻らない
-
wp_next_scheduled()で確かめて、無ければ入れ直す。WordPress 本体も同じことをしている - 確かめるのは、5マイクロ秒程度でクエリも増えない
- 設定でオフにできる処理なら、オンのときだけ入れ直す
- 繰り返しの予定は、時刻が違えば重複して入る。入れる前の確認を省かない
あなたのプラグインの定期処理は、予定が消えたら、次にいつ戻りますか。
手元で試すコマンド
WordPress のディレクトリの隣に置いて、PHP の CLI で動かします。
<?php
// req.php:php req.php admin|front でリクエスト1回
if (($argv[1] ?? '') === 'admin') define('WP_ADMIN', true);
define('WP_USE_THEMES', false);
define('DISABLE_WP_CRON', true);
require __DIR__ . '/wp/wp-load.php';
$hook = 'my_daily_event';
$n = wp_next_scheduled($hook);
echo $argv[1], ': ', $n ? '+' . ($n - time()) . 's' : 'なし', "\n";
<?php
// op.php:php op.php clear|nocron で予定を消す
define('WP_USE_THEMES', false);
define('DISABLE_WP_CRON', true);
require __DIR__ . '/wp/wp-load.php';
if ($argv[1] === 'clear') wp_clear_scheduled_hook('my_daily_event');
if ($argv[1] === 'nocron') delete_option('cron');
php op.php clear && php req.php front && php req.php admin
my_daily_event を、入れ直しの処理を足したプラグインの予定名に変えて使います。
ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。
https://raplsworks.com/