代表的な面接トピック

コーディング面接: asyncio で動的なデッドラインをどのように管理すべきか?

コーディング難しい
Offer.cc 編集チーム公開日 更新日

質問

非同期リクエストでは、上流のメタデータを読み取った後にのみ残りの予算が判明します。asyncio.timeout または timeout_at を使用してこれを設計し、reschedule、expired、CancelledError、TimeoutError、ネスト、および wait_for について説明してください。

プロンプトとコンテキスト

非同期リクエストは、下流呼び出しの予算がわかる前にルーティングメタデータを読み取ります。この設計では、イベントループの単調増加クロックを使用し、初期デッドラインなしでタイムアウトを作成し、メタデータが到着した時点で絶対デッドラインを再スケジュールし、呼び出し元のキャンセルとタイムアウトを区別して保持する必要があります。

Python のドキュメントでは、asyncio.timeout() は再スケジュール可能な非同期コンテキストマネージャーとして、timeout_at() はイベントループのクロックに基づく絶対デッドラインとして説明されています。コンテキストによって引き起こされたキャンセルは、コンテキストの外側で TimeoutError に変換されます。

面接官がテストしていること

相対的な期間と絶対デッドライン、業務上のタイムアウトと外部からのキャンセルの明確な区別に加え、reschedule()expired()、ネスト、および wait_for() の正しい使用法を評価します。優れた回答では、すべての I/O に単一の予算を伝播し、リソースをクリーンアップします。

最初に尋ねるべき確認の質問

  • どのコンポーネントが予算を所有し、メタデータの読み取りにどれだけ消費されましたか?
  • HTTP クライアント、データベース、およびキューはタイムアウトまたはキャンセル信号を受け入れますか?
  • 子呼び出しは 1 つの絶対デッドラインを共有すべきですか、それとも独立した予算を持つべきですか?
  • タイムアウトはデグレードされた応答、リトライ、またはエラーのどれに該当しますか?
  • 本番環境での最小 Python バージョンは何ですか?

30秒の回答フレームワーク

「私は loop.time() を使用して絶対デッドラインを計算します。asyncio.timeout(None) で開始し、メタデータが到着した後に cm.reschedule(deadline) を呼び出します。コンテキストによって生成されたキャンセルは、終了時に TimeoutError になるため、外側でそれをキャッチします。外部の CancelledError はクリーンアップ後もキャンセルのままとなります。すべての下流操作は残りの予算を受け取り、ネストされた呼び出しが完全なタイムアウトをリセットすることはできません。診断には cm.expired() を使用し、競合状態をテストします。」

ステップごとの詳細解説

ステップ 1: 単調クロックでデッドラインを表現する

クロック修正によって時刻がジャンプする可能性があるため、ウォールクロック時刻から残り時間を計算しないでください。loop.time() を使用し、すべての下流操作に 1 つの絶対デッドラインを渡します。

python
loop = asyncio.get_running_loop()
deadline = loop.time() + 2.0
async with asyncio.timeout_at(deadline):
    await call_dependency(deadline)

ステップ 2: 再スケジュール可能なコンテキストを開始する

予算が不明な場合は、asyncio.timeout(None) as cm を使用します。メタデータが到着した後、絶対デッドラインを計算して cm.reschedule(deadline) を呼び出します。予備的な作業と下流呼び出しが 1 つの境界を共有するように、1 つのコンテキストを維持します。

ステップ 3: 残りの予算を伝播する

各クライアントは deadline - loop.time() を計算し、正でない値は即時失敗として処理します。同じデッドラインを渡すことで、ルーティング、データベース、および HTTP レイヤーがそれぞれ完全な相対タイムアウトを付与するのを防ぎます。

ステップ 4: キャンセルからタイムアウトへの変換を理解する

コンテキストは内部で現在の Task をキャンセルし、終了時にそのキャンセルを TimeoutError に変換します。したがって、TimeoutErrorasync with の内側ではなく、外側でキャッチする必要があります。

python
try:
    async with asyncio.timeout(1.0):
        await slow_call()
except TimeoutError:
    return degraded_result()

ステップ 5: 外部キャンセルを保持する

クライアントが切断された場合や親がリクエストをキャンセルした場合、CancelledError は業務上のタイムアウトではありません。接続、ロック、一時ファイルを解放した上で、デグレードされた成功を返すのではなく、再度例外を raise してください。

ステップ 6: timeout、timeoutat、および waitfor を比較する

timeout(delay) は相対的な遅延を使用し、再スケジュール可能です。timeout_at(when) は絶対的な単調デッドラインを受け取り、伝播に最適です。wait_for(aw, timeout) は 1 つの awaitable を対象とし、タイムアウト時にそれをキャンセルし、キャンセルの完了を待つ場合があります。多数の wait_for 呼び出しを組み合わせると、リクエストの予算を超過する可能性があります。

ステップ 7: ネストと期限切れを処理する

タイムアウトコンテキストはネストできます。内側のデッドラインは外側のデッドラインを超えてはなりません。終了後、cm.expired() はコンテキストが実際にデッドラインに達したかどうかを示します。通常の例外、外部キャンセル、および成功結果は、引き続き区別可能である必要があります。

ステップ 8: 競合状態とクリーンアップをテストする

遅延したメタデータ、すでに期限切れのデッドライン、内側優先のタイムアウト、同時の外部キャンセルとタイムアウト、キャンセルを無視する下流操作、reschedule(None)、繰り返される再スケジュール、およびクリーンアップの失敗をテストします。機密ペイロードを含めずに、デッドライン、残りの予算、キャンセル理由、および下流の所要時間を記録します。

高品質な模範回答

loop.time() を使用して 1 つの絶対デッドラインを作成し、それを下流に渡します。予算が不明な場合は timeout(None) に入り、メタデータ到着後に再スケジュールします。コンテキストは内部キャンセルをブロック外で TimeoutError に変換し、外部の CancelledError はそのまま伝播し続けます。ネストされた呼び出しは最も早いデッドラインを使用し、クライアントとデータベースは残りの予算を受け取ります。競合テストを実施して、Task やリソースのリークがないことを検証します。」

よくある間違い

  • デッドラインに time.time() を使用する → クロック修正により予算が変動する → loop.time() を使用する。
  • コンテキスト内で TimeoutError をキャッチする → 変換された例外はそこには存在しない → 外側でキャッチする。
  • 外部キャンセルをタイムアウトとして扱う → クライアントのキャンセルが誤ってデグレードされた結果になる → 例外タイプを区別して保持する。
  • すべてのレイヤーで完全なタイムアウトを付与する → 合計レイテンシが契約値を超える → 1 つの絶対デッドラインを伝播する。
  • wait_for のキャンセル待機を無視する → 実際の所要時間が数値上のタイムアウトを超える → キャンセルの収束を計測する。
  • 成功と単一タイムアウトのみをテストする → 競合パスでリソースがリークする → 再スケジュール、同時キャンセル、およびクリーンアップの失敗をテストする。

フォローアップの質問と優れた回答

フォローアップ 1: なぜ timeout_at はレイヤーごとの timeout よりも安定しているのですか?

すべてのレイヤーが同じ絶対時刻を対象とするため、相対時間の丸めや予算の重複付与によってリクエスト契約を超過することがなくなります。

フォローアップ 2: reschedule に過去のデッドラインが渡された場合はどうなりますか?

コンテキストは次のイベントループの機会に期限切れとなります。再スケジュールする前に残りの予算を確認し、すでに枯渇している場合はキャンセル不可能な I/O の開始を避けます。

フォローアップ 3: データベースはどのようにデッドラインに従いますか?

残り秒数をステートメントタイムアウトまたはキャンセル API に渡します。Python の Task をキャンセルするだけでは、サーバー側のクエリが停止するとは限りません。

フォローアップ 4: キャンセルとタイムアウトが競合した場合、何を記録しますか?

親のキャンセル理由とキャンセル回数を保持し、外部キャンセルを伝播させます。すべての終了を業務上のタイムアウトとしてラベル付けするのではなく、expired() を診断フィールドとして保存します。

フォローアップ 5: どのような場合に wait_for を選択しますか?

単純な相対制限を持つ 1 つの awaitable に対して使用します。呼び出し間で動的なデッドラインやリクエスト全体での予算を共有する場合は、timeout または timeout_at を優先します。

公開情報ソース

関連する質問

関連面接ツール

コーディング問題にはスクリーンショットを使用

問題をキャプチャし、制約条件、解法アプローチ、コード、エッジケース、計算量の順に進めます。

ツールを見る