[インデックス 15307] ファイルの概要
このコミットは、Go言語のコードレビューシステムにおいて、HTTPフェッチ処理がSSLハンドシェイク中に30秒以上ブロックする問題に対処するためのものです。具体的には、lib/codereview/codereview.py
ファイルにソケットタイムアウト設定を追加し、この問題を緩和することを目的としています。
コミット
f19cf640d482480432e65a451477cd3bcf818288 Author: Russ Cox rsc@golang.org Date: Tue Feb 19 10:18:16 2013 -0500
codereview: give up on http fetch after 30 seconds
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f19cf640d482480432e65a451477cd3bcf818288
元コミット内容
codereview: give up on http fetch after 30 seconds
If Python blocks in the SSL handshake it seems to be
completely uninterruptible, and I've been seeing it
block for at least hours recently. I don't know if the
problem is on the client side or the server side or
somewhere in the network, but setting the timeout
at least means you're guaranteed a new shell prompt
(after printing some errors).
R=golang-dev, bradfitz, minux.ma
CC=golang-dev
https://golang.org/cl/7337048
変更の背景
このコミットの背景には、Go言語のコードレビューシステムが利用していたPythonスクリプト(lib/codereview/codereview.py
)が、HTTPフェッチ、特にSSLハンドシェイクの段階で無限にブロックしてしまうという深刻な問題がありました。コミットメッセージによると、このブロックは「少なくとも数時間」続くことがあり、ユーザーが新しいシェルプロンプトを得ることを妨げていました。
当時のPythonのSSL実装、特にsocket
モジュールとSSL/TLSハンドシェイクの連携において、特定の条件下でブロックが発生すると、その処理が完全に中断不可能になるという問題が指摘されています。これは、ネットワークの問題(例:ファイアウォール、プロキシ、不安定な接続)、サーバー側の問題(例:応答しないSSLサーバー、証明書の問題)、またはクライアント側のPython SSLライブラリのバグなど、様々な要因によって引き起こされる可能性がありました。
このような無限ブロックは、開発者のワークフローを著しく阻害します。コードレビューの提出や更新といった基本的な操作が完了せず、CLIツールが応答しなくなるため、ユーザーは手動でプロセスを強制終了するしかありませんでした。これは生産性の低下だけでなく、ツールの信頼性に対する不信感にもつながります。
このコミットは、問題の根本原因を特定して修正するのではなく、暫定的な対策としてタイムアウトを設定することで、少なくともユーザーがツールから解放され、新しい操作を開始できるようにすることを目的としています。これにより、無限ブロックによるユーザー体験の悪化を緩和し、ツールの可用性を向上させることができました。
前提知識の解説
1. SSL/TLSハンドシェイク
SSL (Secure Sockets Layer) およびその後継であるTLS (Transport Layer Security) は、インターネット上で安全な通信を行うための暗号化プロトコルです。クライアントとサーバーが暗号化された通信を開始する前に、互いの身元を確認し、暗号化パラメータを確立するプロセスを「ハンドシェイク」と呼びます。
ハンドシェイクの主要なステップは以下の通りです。
- ClientHello: クライアントがサーバーに接続を要求し、サポートするSSL/TLSバージョン、暗号スイート(暗号化アルゴリズムの組み合わせ)、圧縮方式などを通知します。
- ServerHello: サーバーがClientHelloに応答し、クライアントが提案した中から選択したSSL/TLSバージョン、暗号スイート、セッションIDなどを通知します。
- Certificate: サーバーが自身のデジタル証明書をクライアントに送信します。クライアントはこの証明書を検証し、サーバーの身元を確認します。
- ServerKeyExchange (オプション): サーバーが鍵交換に必要な追加情報(例:Diffie-Hellmanパラメータ)を送信します。
- ServerHelloDone: サーバーがハンドシェイクのサーバー側の部分を完了したことを示します。
- ClientKeyExchange: クライアントが鍵交換に必要な情報(例:PreMasterSecret)を生成し、サーバーの公開鍵で暗号化して送信します。
- ChangeCipherSpec: クライアントがこれ以降の通信を暗号化されたチャネルで行うことをサーバーに通知します。
- Encrypted Handshake Message: クライアントがハンドシェイクの完了メッセージを暗号化して送信します。
- ChangeCipherSpec: サーバーがこれ以降の通信を暗号化されたチャネルで行うことをクライアントに通知します。
- Encrypted Handshake Message: サーバーがハンドシェイクの完了メッセージを暗号化して送信します。
このハンドシェイクプロセス中に、ネットワークの遅延、パケットロス、サーバーの応答遅延、または証明書の検証問題などが発生すると、処理が停止したり、タイムアウトしたりする可能性があります。
2. ソケットとタイムアウト
「ソケット」は、ネットワーク通信のエンドポイントを抽象化したものです。プログラムはソケットを通じてデータを送受信します。ソケット通信では、相手からの応答を待つ際に、無限に待ち続けることを避けるために「タイムアウト」を設定することが一般的です。
- ブロッキングソケット: デフォルトでは、ソケット操作(例:データの読み書き、接続の確立)はブロッキングモードで動作します。これは、操作が完了するかエラーが発生するまで、プログラムの実行が停止することを意味します。
- ソケットタイムアウト: タイムアウトを設定すると、ブロッキング操作が指定された時間内に完了しなかった場合に、エラー(通常はタイムアウト例外)が発生し、プログラムがブロック状態から解放されます。これにより、アプリケーションが応答不能になることを防ぎ、エラー処理や再試行ロジックを実装できるようになります。
3. Pythonの socket
モジュールと setdefaulttimeout
Pythonの標準ライブラリには、低レベルのネットワーク通信を扱うための socket
モジュールが含まれています。このモジュールは、TCP/IPソケットの作成、接続、データの送受信などの機能を提供します。
socket.setdefaulttimeout(timeout)
関数は、socket
モジュールで作成されるすべての新しいソケットに対して、デフォルトのタイムアウト値を設定します。
timeout
引数は秒単位で指定します。timeout
がNone
の場合、ソケットはブロッキングモードになり、タイムアウトは設定されません(無限に待機します)。- この関数は、既存のソケットには影響を与えません。新しいソケットにのみ適用されます。
- この設定はグローバルであり、アプリケーション全体に影響を与えるため、注意して使用する必要があります。
このコミットでは、この setdefaulttimeout
を利用して、HTTPフェッチ処理におけるSSLハンドシェイクの無限ブロックを回避しようとしています。
技術的詳細
このコミットは、Pythonの socket
モジュールが提供するグローバルなデフォルトタイムアウト設定機能を利用して、HTTPフェッチ処理、特にSSLハンドシェイク中の無限ブロック問題を緩和します。
問題の根源
コミットメッセージが示唆するように、当時のPythonのSSLハンドシェイク実装には、特定の条件下で「完全に中断不可能」なブロッキングが発生する問題がありました。これは、通常のソケット操作のタイムアウトが適用されない、あるいはタイムアウト設定が適切に機能しないような、より低レベルのOSコールやSSLライブラリの内部状態に起因する可能性が高いです。例えば、TCP接続の確立はタイムアウトしても、その後のSSLハンドシェイク(特に証明書の交換や鍵のネゴシエーション)が、相手からの応答がない場合に無限に待機してしまうような状況が考えられます。
このような状況は、以下のような場合に発生しやすくなります。
- ネットワークの中間機器: ファイアウォールやプロキシがSSLハンドシェイクの特定のパケットをドロップしたり、遅延させたりする場合。
- サーバー側の問題: SSLサーバーが応答しない、またはハンドシェイクの途中でハングアップする場合。
- クライアント側のSSLライブラリのバグ: Pythonが内部的に使用するOpenSSLなどのライブラリに、特定の条件下でデッドロックや無限ループを引き起こすバグが存在する場合。
socket.setdefaulttimeout
の適用
このコミットでは、MySend1
関数内でHTTPリクエストを送信する直前に、socket.setdefaulttimeout(timeout)
を呼び出しています。ここで timeout
は30秒に設定されています。
この変更の技術的なポイントは以下の通りです。
- グローバルな影響:
socket.setdefaulttimeout
は、その関数が呼び出された時点以降に作成されるすべての新しいソケットに影響を与えます。MySend1
関数がHTTPリクエストのために新しいソケットを作成する場合、そのソケットには30秒のタイムアウトが適用されます。 - ハンドシェイクへの適用: 通常、ソケットのタイムアウトは接続確立やデータ送受信だけでなく、SSL/TLSハンドシェイクの各フェーズにも適用されます。これにより、ハンドシェイクの途中で相手からの応答が30秒以上ない場合、タイムアウトエラーが発生し、ブロッキング状態から抜け出すことができます。
- エラー処理: タイムアウトが発生すると、Pythonは
socket.timeout
例外を発生させます。コミットメッセージにある「(after printing some errors)」という記述は、この例外が捕捉されずにスタックトレースが出力されることを示唆しています。これは、ユーザーに問題が発生したことを通知し、シェルプロンプトを返すための意図的な動作であると考えられます。 - 元のタイムアウトの復元: 変更されたコードでは、
socket.setdefaulttimeout(timeout)
を呼び出す前にold_timeout = socket.getdefaulttimeout()
で元のデフォルトタイムアウトを保存し、処理の最後にsocket.setdefaulttimeout(old_timeout)
で元の値に戻しています。これは、この関数が他のソケット操作に与える影響を局所化するための重要なベストプラクティスです。これにより、MySend1
の呼び出しが完了した後、他のネットワーク操作が意図しないタイムアウト設定の影響を受けないようにします。
限界とトレードオフ
この解決策は、無限ブロックを回避するための効果的な暫定措置ですが、いくつかの限界とトレードオフがあります。
- 根本原因の未解決: この変更は、SSLハンドシェイクがブロックする根本原因(ネットワーク、サーバー、Pythonライブラリのバグなど)を解決するものではありません。単に、ブロックが一定時間続いた場合に強制的に終了させるだけです。
- エラーメッセージ: タイムアウトが発生した場合、ユーザーにはエラーメッセージが表示されます。これは、操作が失敗したことを意味し、ユーザーは再試行するか、問題の原因を調査する必要があります。
- 適切なタイムアウト値: 30秒という値は、通常のネットワーク遅延を考慮しつつ、無限ブロックを避けるための経験的な値です。ネットワーク状況によっては、30秒では短すぎる場合や、逆に長すぎる場合もあります。
- グローバルな設定の注意点:
setdefaulttimeout
はグローバルな設定であるため、もしold_timeout
の復元が適切に行われなかったり、他のスレッドが同時にソケットを作成したりする場合、意図しない副作用が発生する可能性があります。ただし、このコミットのコードでは復元処理が適切に行われています。
このコミットは、ユーザー体験の悪化を最小限に抑えつつ、ツールの応答性を確保するための実用的なアプローチとして評価できます。
コアとなるコードの変更箇所
変更は lib/codereview/codereview.py
ファイルに対して行われています。
--- a/lib/codereview/codereview.py
+++ b/lib/codereview/codereview.py
@@ -2444,6 +2444,8 @@ def MySend1(request_path, payload=None,
self._Authenticate()
if request_path is None:
return
+ if timeout is None:
+ timeout = 30 # seconds
old_timeout = socket.getdefaulttimeout()
socket.setdefaulttimeout(timeout)
コアとなるコードの解説
変更は MySend1
という関数内で行われています。この関数は、おそらくGoのコードレビューシステムが利用するHTTPリクエストを送信するための内部ヘルパー関数です。
-
if timeout is None:
:- この行は、
MySend1
関数にtimeout
引数が明示的に渡されなかった場合(またはNone
が渡された場合)をチェックしています。 - これは、この関数が呼び出される際に、呼び出し元が特定のタイムアウト値を指定しない場合に、デフォルトのタイムアウトを適用するための条件です。
- この行は、
-
timeout = 30 # seconds
:- 上記の条件が真の場合、
timeout
変数に30
が代入されます。これは、HTTPフェッチ操作のタイムアウトを30秒に設定することを意味します。
- 上記の条件が真の場合、
-
old_timeout = socket.getdefaulttimeout()
:socket.getdefaulttimeout()
を呼び出して、現在のPythonのグローバルなデフォルトソケットタイムアウト値を取得し、old_timeout
変数に保存しています。- これは、この関数の処理が完了した後に、元のデフォルトタイムアウト設定を復元するために必要です。
-
socket.setdefaulttimeout(timeout)
:socket.setdefaulttimeout()
を呼び出し、先ほど設定したtimeout
(30秒) を新しいグローバルなデフォルトソケットタイムアウトとして設定しています。- これにより、この行以降に作成されるすべての新しいソケット(
MySend1
関数内でHTTPリクエストのために作成されるソケットを含む)は、30秒のタイムアウトを持つようになります。
この変更により、MySend1
関数がHTTPリクエストを送信する際に、SSLハンドシェイクを含むネットワーク操作が30秒を超えてブロックすることがなくなります。30秒以内に応答がない場合、ソケットはタイムアウトエラーを発生させ、プログラムはブロック状態から解放されます。
関連リンク
- https://golang.org/cl/7337048 (Go Code Reviewのチェンジリスト)
参考にした情報源リンク
- Python
socket
モジュールのドキュメント (当時のバージョンに基づく) - SSL/TLSハンドシェイクに関する一般的な情報源
- Go言語のコードレビューシステムに関する情報 (当時の設計に基づく)
- 当時のPythonのSSL実装に関する既知の問題や議論 (もしあれば)
(注:具体的な参考情報源のURLは、2013年当時のPythonのドキュメントやコミュニティの議論を特定する必要があるため、ここでは一般的なカテゴリのみを記載しています。)