【ITニュース解説】Process Local Telegram getUpdates in PHP Without Replaying Failed Actions
2026年10月30日に「Dev.to」が公開したITニュース「Process Local Telegram getUpdates in PHP Without Replaying Failed Actions」について初心者にもわかりやすく解説しています。
ITニュース概要
PHPでTelegramボットをローカル開発する際、getUpdatesロングポーリングはcURLタイムアウトやエラー時の重複処理で問題が起こりがち。これを防ぐには、cURLタイムアウトを長めに設定し、処理済み更新IDをDBに永続化、各処理を個別に完結させると、堅牢なボットを作れる。
ITニュース解説
Telegramボットを開発する際、特にローカル環境で試行錯誤を繰り返すとき、外部に公開されたHTTPSエンドポイント(Webhook)を設定するのは手間がかかる。そこで、Long Pollingという方法が代替手段としてよく用いられる。これは、ローカルで実行されるPHPスクリプトが、Telegramのサーバーから直接アップデート(ユーザーからのメッセージなど)を取得する仕組みである。
しかし、このLong Pollingを単純に実装すると、いくつかの問題が発生することがある。例えば、スクリプトがフリーズしたり、アップデートが適切に処理されずに失われたり、致命的なエラーが発生した後に同じアップデートが繰り返し処理されたりする。もしボットがデータベースへの書き込みや決済処理などの外部への影響を伴う動作をする場合、このような問題は大きな損害につながる可能性がある。本記事では、cURLのタイムアウトを適切に処理し、アップデートのオフセット(どこまで処理したかを示す位置情報)を正確に進め、ローカルでの実行失敗時に重複処理を防ぐ、堅牢なLong Pollingの構築方法を解説する。
まず、Long Pollingの誤ったアプローチとその問題点について説明する。TelegramのgetUpdatesエンドポイントにリクエストを送信する際、timeoutというパラメータで、Telegramが新しいアップデートを待つ時間を秒単位で指定できる。もしtimeoutを0に設定するか省略した場合、Telegramは利用可能なアップデートがあればすぐに返し、なければ空の配列を返す。これをwhile(true)ループで頻繁に呼び出す方法は「Short Polling」と呼ばれ、ネットワークに過度の負荷をかけ、CPUリソースを消費し、Telegram APIのレート制限にすぐに引っかかるため推奨されない。
正しくLong Pollingを利用するには、timeout=30のように具体的な値を指定する。これにより、TelegramはHTTP接続を最大30秒間維持し、その間に新しいアップデートが到着するのを待つ。問題は、このサーバー側の待ち時間に対して、PHPのcURLクライアントのタイムアウト設定が適切でない場合に発生する。例えば、Telegramが30秒間接続を保持するように設定しているのに、cURLクライアントのタイムアウトも30秒に設定されていると、ネットワークのわずかな遅延により、Telegramからの応答が届く直前(例えば30.001秒目)にcURLが接続を切断してしまうことがある。この結果、PHPスクリプトはcURLエラーとして処理を中断し、データを受け取れないまま再試行を繰り返すことになる。
さらに、もう一つの問題は、スクリプトがクラッシュした際に発生する「リプレイループ」である。初期のLong Pollingの実装では、$offset変数をPHPスクリプトのメモリ上でのみ管理することが多い。$offsetは、次にどのupdate_idからアップデートを取得するかをTelegramに伝えるための重要な情報である。もし、複数のアップデートをまとめて処理している途中で、未処理の例外や致命的なエラーが発生し、スクリプトが停止した場合、メモリ上の$offsetの値は失われる。その結果、スクリプトを再起動すると、$offsetが初期値の0に戻ってしまい、Telegramは以前に処理を開始したバッチのアップデートを最初からすべて再送してしまう。これにより、既に処理されたアップデートが再度処理され、データベースへの重複書き込みや二重決済といった予期せぬ副作用が発生する危険性がある。
これらの問題を解決するには、いくつかの修正が必要である。まず、cURLタイムアウトの不一致の問題を解決するため、cURLクライアントのタイムアウト設定(CURLOPT_TIMEOUT)は、Telegramに指定するLong Pollingのタイムアウト値よりも常に長く設定する必要がある。例えば、Telegramに30秒待つよう指示するなら、cURLには40秒待つよう指示するのが安全な経験則である。これにより、ネットワーク遅延によってcURLがTelegramからの応答を待たずにタイムアウトしてしまうのを防ぐ。また、Telegram APIからのすべての応答に対して、HTTPステータスコードが200であることを確認し、さらにレスポンスに含まれるokフィールドがtrueであるかを検証することが不可欠である。これにより、APIからのエラー応答や無効なJSONデータを適切に検出し、処理を進める前にエラーとして扱える。
さらに、allowed_updatesパラメータをgetUpdatesリクエストに追加することで、アプリケーションが処理を期待するアップデートの種類(例:message、callback_query)を明示的に宣言できる。これにより、開発中にボットがパブリックグループに追加された場合に、予期せぬアップデートタイプ(例:channel_post、poll_answer)が処理ループを詰まらせるのを防ぐことができる。
最も重要な改善は、「べき等処理」と「状態の永続化」である。べき等処理とは、同じ操作を何度実行しても同じ結果になるようにすることである。これを実現するためには、update_idの処理状況をメモリ変数に頼るのではなく、ローカルディスク上の永続的なストア(例えばSQLiteデータベースやJSONファイル)に保存する必要がある。これにより、スクリプトがクラッシュしたり手動で終了されたりしても、次に起動したときに以前の処理状態を正確に復元し、どこから処理を再開すべきかを知ることができる。
具体的には、データベースにlast_offsetという値を保存し、getUpdatesを呼び出す前にこの値を取得して利用する。そして、各アップデートを処理した後、またはエラーが発生した場合でも、次に処理すべきupdate_id(通常は現在のupdate_idに1を加えた値)をデータベースに保存し直す。さらに、既に処理したアップデートのupdate_idを別のテーブルに記録しておくことで、仮にgetUpdatesが過去のアップデートを再度返してきたとしても、重複して処理されるのを防ぐことができる。個々のアップデートのビジネスロジックは、それぞれ独立したtry/catchブロックで囲むべきである。これにより、たとえ一つのアップデートの処理中にエラーが発生しても、そのエラーがLong Pollingプロセス全体を停止させず、エラーをログに記録しつつ、次のアップデートの処理へ進めることができる。これにより、単一の不正なアップデートが全体のシステム停止を引き起こすのを防ぎ、堅牢性を高める。
最後に、Long Pollingはローカル開発には非常に有用だが、本番環境に移行する際には、Webhookとのトレードオフを理解しておく必要がある。Telegramは、ボットトークンごとにWebhookとLong Pollingのいずれか一方の配信メカニズムしか許可しない。もしsetWebhookでWebhook URLを設定した場合、getUpdatesを呼び出すとHTTP 409 Conflictエラーが発生する。本番環境でWebhookを使用していたボットをローカルでLong Pollingでテストしたい場合は、まずdeleteWebhookを呼び出してWebhookを削除する必要がある。
また、PHPのCLIスクリプトをwhile(true)ループで長時間実行すると、メモリリークが発生し、時間の経過とともにメモリが蓄積される可能性がある。本番環境でLong Pollingを意図的に使用する場合(例:ファイアウォールの内側でパブリックIPアドレスがない場合)、supervisordのようなプロセス監視ツールを使用し、スクリプトを定期的に(例えば1,000サイクルごとや1時間ごと)再起動させる設定をすることで、クリーンなメモリ状態で実行を継続できる。
さらに、getUpdatesはアップデートをupdate_idの昇順で順次配信するため、もし単一のアップデートの処理に時間がかかる場合(例:大規模な言語モデルAPIの呼び出しや画像のレンダリングなど)、その処理が完了するまでLong Pollingプロセス全体がブロックされ、他のユーザーのアップデート処理が遅延する。高スループットが求められるボットの場合、Long Pollingで受信したアップデートのペイロードを非同期ジョブキュー(RedisやRabbitMQなど)に渡し、複数のワーカープロセスで並行してメッセージを処理するアーキテクチャがより適している。
Telegramインテグレーションを構築する際には、厳密なオフセット同期と堅牢な例外処理を維持することが、状態の破損や重複実行を防ぐ上で極めて重要である。