WordPress の投稿公開をトリガーに、公開を遅くせず EventBridge へイベントを送る
本番稼働中のプラグインで、投稿の公開・更新・削除を EventBridge へ送る処理を、保存リクエストをブロックしないよう WP-Cron で非同期化する。公開ボタンを押してから数秒待たされる体感を解消した手順と、重複予約の落とし穴をまとめます。
目次
実際に稼働させているプラグインで、投稿の公開・更新・削除を外部(Amazon EventBridge)へイベントとして飛ばしています。最初は、この送信を投稿の保存処理に同期でぶら下げていました。
すると、公開ボタンを押してから実際に公開されるまで数秒待たされます。エラーになるわけではないのですが、編集して保存するたびに引っかかるこの体感が、地味に良くありませんでした。
送信を WordPress の保存リクエストから切り離して、WP-Cron で非同期に動かしたら、公開はすぐ返るようになりました。投稿の状態変化をどこで拾い、どこで送信を非同期化するか、手順をまとめておきます。
やりたかったことと前提
やりたかったのは、投稿が公開・更新・削除されたときに、その事実を EventBridge へイベントとして飛ばすことです。受け取った側が要約生成や通知などを動かす、という構成を想定しています。
- WordPress のプラグイン(またはテーマの
functions.php)にコードを書ける - 送信先の EventBridge イベントバスが用意してある
- EventBridge へ実際にイベントを送る関数(後述の
send_to_eventbridge()に相当する送信処理)が手元にある
この記事では、送信処理そのものの中身(認証や署名の実装)は扱いません。あくまで「いつ送るか」と「保存リクエストをブロックせずに送るか」に絞ります。
投稿の状態変化をフックする
投稿の状態が変わったときに発火するのが transition_post_status(投稿の状態遷移フック)です。新しい状態・古い状態・投稿オブジェクトの 3 つを受け取れます。
add_action( 'transition_post_status', 'my_capture_status', 10, 3 );
function my_capture_status( $new_status, $old_status, $post ) {
// 「初めて公開された」ときだけを対象にする
if ( 'publish' === $new_status && 'publish' !== $old_status ) {
my_enqueue_event( $post->ID, 'post.published' );
}
}
ここに落とし穴があります。transition_post_status という名前から「状態が変わったときだけ動く」と思いがちですが、実際には 状態が変わらない通常の更新でも発火します。公開済みの記事をあとから少し直して保存しただけでも、$new_status が publish のまま発火します。
そのため「初めて公開されたとき」だけを拾いたいなら、上のコードのように $old_status !== 'publish' を必ず添えます。これを忘れると、記事を編集するたびに post.published が飛んでしまいます。
削除のときは別のフック before_delete_post を使います。
add_action( 'before_delete_post', function ( $post_id ) {
my_enqueue_event( $post_id, 'post.deleted' );
} );
before_delete_post は投稿が完全に削除される直前に発火します。ゴミ箱に入れただけでは発火しないので、ゴミ箱を経由する運用なら wp_trash_post フックも合わせて検討します。
post.published / post.deleted のような文字列は、あとで EventBridge 側の DetailType にそのまま渡します。受信側はこの値でルーティングするので、ここで決めた名前が契約になります。
重い送信を保存リクエストに乗せない
ここがこの記事の本題です。上の my_enqueue_event() の中で EventBridge への送信を そのまま実行してしまうと、投稿の保存リクエストが送信完了まで待たされます。公開ボタンを押してから数秒待たされていたのは、これが原因でした。
そこで送信を wp_schedule_single_event(一度だけ実行する予約)に逃がします。
function my_enqueue_event( $post_id, $detail_type ) {
wp_schedule_single_event( time(), 'my_send_event', array( $post_id, $detail_type ) );
}
add_action( 'my_send_event', 'my_send_event_handler', 10, 2 );
function my_send_event_handler( $post_id, $detail_type ) {
// ここで EventBridge にイベントを送る(送信処理の中身はこの記事では扱わない)
send_to_eventbridge( $detail_type, array( 'post_id' => $post_id ) );
}
ポイントは、wp_schedule_single_event( time(), ... ) と書いても、その場で送信が走るわけではないことです。WP-Cron はサイトに次のアクセスがあったときに予約を実行する簡易スケジューラなので、予約した時点では保存リクエストはすぐ返ります。重い送信は、後続のリクエストのタイミングで別途実行されます。これで公開ボタンを押したあとすぐ画面が返るようになりました。
送信先の EventBridge PutEvents では、Source・DetailType・Detail の 3 つが必須です。どれかが欠けるとそのエントリは失敗するので、send_to_eventbridge() の中ではこの 3 つを必ず組み立てて渡します。DetailType には先ほど決めた post.published などを入れます。
同じ記事を立て続けに保存したときの重複に注意
wp_schedule_single_event には、知らないと送信が「飛ばない」側に転ぶ仕様があります。同じアクションフックの既存予約の 10 分以内 に、同じ $args で予約しようとすると、その予約は無視されます。重複判定は同一フックに対して md5( serialize( $args ) ) をキーに行われます。
今回の構成は $args に $post_id が入るので、別々の記事の公開は別イベントとして通ります。ここは問題ありません。引っかかるのは、同じ記事に同じ $detail_type を 10 分以内に連続で投げたとき です。たとえば 1 つの記事を短時間に何度も更新すると、2 回目以降の post.updated が静かにドロップします。
毎回確実に送りたいなら、$args に一意な値(タイムスタンプ等)を足して重複判定をかわすか、「10 分以内の連投は 1 回にまとまってよい」と割り切れる設計かを、ここで判断しておくのが安全だと考えています。
注意点と限界
WP-Cron はサイトへのアクセスを起点に動くため、トラフィックがほとんど無いサイトでは、予約した送信の実行が遅れます。公開した瞬間にイベントが飛ぶことは保証されません。送信のリアルタイム性が重要なら、DISABLE_WP_CRON を有効にしてサーバーの cron から wp-cron.php を定期実行する、といった対応が要ります。
もう一つは前述のガードです。transition_post_status が更新でも発火する性質を忘れて $old_status !== 'publish' を外すと、記事を直すたびに post.published が飛びます。受信側が「公開された」前提で動いていると、重複して処理が走ります。
まとめ
投稿の状態変化は transition_post_status と before_delete_post で拾い、重い送信は wp_schedule_single_event で WP-Cron に逃がす。これだけで、投稿の保存リクエストをブロックせずに外部へイベントを送れます。
押さえておく注意点は 3 つです。transition_post_status は通常の更新でも発火すること。WP-Cron はアクセス起点なので低トラフィックだと遅延すること。そして 10 分以内に同じ $args で予約した同一フックは重複扱いでドロップすること。この 3 つだけ押さえておけば、あとは送信処理を差し込むだけで動きます。