今日のゴール
- ブロッキング操作のタイムアウト設定を理解する
- タイムアウト例外の適切な処理方法を学ぶ
- 実践的なタイムアウト戦略を身につける
ブロッキング操作の危険性
readとtakeはマッチするタプルが見つかるまで永久にブロックします。
# ⚠️ 危険: マッチするタプルがなければ永久に待機
let result = await ts.readAsync(toPattern(strVal("nonexistent"), nilValue()))
# ここに到達しない可能性がある
本番環境では、必ずタイムアウトを設定すべきです。
タイムアウトの指定方法
readAsyncとtakeAsyncはtimeoutパラメータを受け取ります。
try:
let result = await ts.readAsync(pattern, timeout = 5000) # 5秒
echo "Found: ", result
except TimeoutError:
echo "Timeout: No matching tuple found within 5 seconds"
タイムアウト値の意味
| 値 | 動作 |
|---|---|
-1 |
タイムアウトなし(永久に待機)デフォルト |
0 |
即座に返る(tryRead/tryTakeと同等) |
> 0 |
指定ミリ秒待機してタイムアウト |
例外処理のパターン
パターン1: try-except
try:
let result = await ts.takeAsync(jobPattern, timeout = 3000)
processJob(result)
except TimeoutError:
echo "No job available, will retry later"
except CatchableError as e:
echo "Unexpected error: ", e.msg
パターン2: 非ブロッキング操作
タイムアウトの代わりにtryRead/tryTakeを使う方法:
var attempts = 0
while attempts < maxAttempts:
let result = await ts.tryReadAsync(pattern)
if result.isSome:
return result.get()
attempts.inc
await sleepAsync(pollInterval)
echo "Gave up after ", maxAttempts, " attempts"
パターン3: デフォルト値を返す
proc readWithDefault(pattern: Pattern, default: Tuple, timeout: int64): Future[Tuple] {.async.} =
try:
return await ts.readAsync(pattern, timeout)
except TimeoutError:
return default
実践的なタイムアウト戦略
戦略1: 指数バックオフ
最初は短いタイムアウトで試し、徐々に長くする。
var timeout = 100 # 初期値
const maxTimeout = 10000
const multiplier = 2
while timeout <= maxTimeout:
try:
return await ts.readAsync(pattern, timeout.int64)
except TimeoutError:
timeout = min(timeout * multiplier, maxTimeout)
利点: 軽い障害なら早く検出、重い障害でも諦めずに待機
戦略2: キャンセル可能な待機
var cancelled = false
proc waitForJob() {.async.} =
while not cancelled:
let result = await ts.tryTakeAsync(jobPattern)
if result.isSome:
return result.get()
await sleepAsync(100)
proc cancel() =
cancelled = true
ベストプラクティス
1. 適切なデフォルト値を設定
const
DEFAULT_READ_TIMEOUT = 5000'i64 # 5秒
DEFAULT_TAKE_TIMEOUT = 10000'i64 # 10秒
LONG_OPERATION_TIMEOUT = 60000'i64 # 1分
2. 操作種別でタイムアウトを変える
| 操作 | タイムアウト | 理由 |
|---|---|---|
| 設定読み込み | 短い(1秒) | 設定はすぐ見つかるべき |
| ジョブ待機 | 長い(30秒) | ジョブは非同期に来る |
| 外部連携 | 非常に長い(120秒) | 外部システムは遅い可能性 |
3. エラーメッセージを明確に
except TimeoutError:
raise newException(TimeoutError,
"Failed to get job of type '" & jobType & "' within 5 seconds")
まとめ
学んだこと
| トピック | ポイント |
|---|---|
| timeout パラメータ | ミリ秒単位で指定 |
| TimeoutError | タイムアウト時に発生 |
| 戦略 | try-except、非ブロッキング、指数バックオフ |
| ベストプラクティス | デフォルト値、操作別設定、明確なエラー |
選び方の指針
| 状況 | 推奨 |
|---|---|
| 結果が必要 | try-except + タイムアウト |
| ポーリング | tryRead/tryTake |
| 断続的な障害 | 指数バックオフ |
| ユーザー操作 | キャンセル可能な待機 |
演習問題
問題8-1: リトライ機構
3回までリトライし、各リトライで待ち時間を2倍にする関数を実装してください。
ヒント
- 初期タイムアウト: 500ms
- リトライごとに: 500ms → 1000ms → 2000ms
- 最後まで失敗したら
none(Tuple)を返す
proc readWithRetry(pattern: Pattern): Future[Option[Tuple]] {.async.} =
var timeout = 500
for attempt in 1..3:
try:
return some(await ts.readAsync(pattern, timeout.int64))
except TimeoutError:
timeout *= 2
return none(Tuple)
問題8-2: タイムアウト付きパイプライン
入力→処理→出力のパイプラインで、各ステージに適切なタイムアウトを設定してください。
ヒント
- 入力ステージ: データソースからの読み取り(5秒)
- 処理ステージ: 変換処理(10秒)
- 出力ステージ: 結果の書き込み(3秒)
- いずれかがタイムアウトしたら、エラーをログに記録して次のデータへ
問題8-3: サーキットブレーカー
連続して3回タイムアウトしたら、30秒間リクエストを拒否する「サーキットブレーカー」を実装してください。
ヒント
- 状態: Closed(正常)、Open(拒否中)、HalfOpen(試行中)
- 連続失敗カウンターを管理
- Open状態では即座にエラーを返す
- 30秒後にHalfOpenに移行して1回試行
前回: 名前付きスペース | 目次 | 次回: イベント通知