Issueの発生
Go製の並行処理ライブラリであるWorkerPoolに、新しいバグ報告が届きました。ユーザーは以下のようなコードを実行した際に、ランタイムパニックが発生することを報告しています。
package main
import (
"context"
"fmt"
"math/rand"
"sync"
"time"
"github.com/example-ai/workerpool"
)
func main() {
var taskRegistry sync.Map
wp := workerpool.New(
workerpool.WithMaxCapacity(20),
workerpool.WithBaseline(3),
)
defer wp.Shutdown()
for id := 1; id <= 20; id++ {
taskID := id
if _, exists := taskRegistry.Load(taskID); !exists {
wp.Submit(func() error {
taskRegistry.Store(taskID, struct{}{})
processTask(taskID)
taskRegistry.Delete(taskID)
return nil
})
}
}
wp.AwaitCompletion()
}
func processTask(id int) error {
delay := rand.Intn(20)
time.Sleep(time.Duration(delay) * time.Second)
fmt.Printf("Task %d completed after %d seconds\n", id, delay)
return nil
}
実行結果は以下の通りです:
$ go run main.go
Task 1 completed after 3 seconds
Task 2 completed after 0 seconds
panic: runtime error: slice bounds out of range [:-4]
goroutine 42 [running]:
github.com/example-ai/workerpool.(*workerPool).scalePool(0x1400010a000)
/Users/dev/go/pkg/mod/github.com/example-ai/workerpool@v0.6.0/pool.go:185 +0x1f8
エラーは常にpool.go:185で発生することが確認できました。
バグの特定
問題のあるコードを抜き出してみましょう。185行目は以下のスライス操作です:
pool.availableIndices = pool.availableIndices[:len(pool.availableIndices)-reductionCount]
この処理は以下のスケーリング関数の中に存在します:
func (pool *workerPool) scalePool() {
ticker := time.NewTicker(pool.scalingDelay)
defer ticker.Stop()
for {
select {
case <-ticker.C:
pool.syncCond.L.Lock()
currentSize := len(pool.taskHandlers)
// 負荷が高い場合はスケールアップ
if len(pool.jobChannel) > currentSize*3/4 && currentSize < pool.capacity {
scaleAmount := min(currentSize*2, pool.capacity) - currentSize
for j := 0; j < scaleAmount; j++ {
handler := newTaskHandler()
pool.taskHandlers = append(pool.taskHandlers, handler)
newIndex := len(pool.taskHandlers) - 1
pool.availableIndices = append(pool.availableIndices, newIndex)
handler.launch(pool, newIndex)
}
}
// アイドル時はスケールダウン
if len(pool.jobChannel) == 0 && currentSize > pool.baselineSize {
reductionCount := max((currentSize-pool.baselineSize)/2, pool.baselineSize)
pool.taskHandlers = pool.taskHandlers[:currentSize-reductionCount]
pool.availableIndices = pool.availableIndices[:len(pool.availableIndices)-reductionCount]
}
pool.syncCond.L.Unlock()
case <-pool.context.Done():
return
}
}
}
AIによる修正提案
問題の核心はスケールダウン時の計算式です。AIアシスタントに分析を依頼したところ、reductionCountの算出方法に誤りがあると指摘されました。
現在の式:max((currentSize-baselineSize)/2, baselineSize)
例えばcurrentSizeが9でbaselineSizeが5の場合:max((9-5)/2, 5) = max(2, 5) = 5となり、9-5=4のworkersを削除することになりますが、reductionCountは5と矛盾しています。
AIは以下の修正を提案しました:
reductionCount := (currentSize - pool.baselineSize + 1) / 2
検証してみましょう:
- currentSize=10, baselineSize=5 →
(10-5+1)/2 = 3→ 7残る - currentSize=7, baselineSize=5 →
(7-5+1)/2 = 1→ 6残る - currentSize=6, baselineSize=5 →
(6-5+1)/2 = 1→ 5残る
これで正しくbaselineSizeまで減少することが確認できます。
残存バグの発見
しかし、この修正だけではまだパニックが発生しました。さらに分析した結果、2つの根本的な問題があることが判明しました。
まず、スケールダウンの条件が不十分です。jobChannelが空でも、workersはまだ実行中の可能性があります。WorkerPoolの内部実装を確認すると:
func (pool *workerPool) acquireHandler() int {
pool.mutex.Lock()
lastIndex := len(pool.availableIndices) - 1
handlerIndex := pool.availableIndices[lastIndex]
pool.availableIndices = pool.availableIndices[:lastIndex]
pool.mutex.Unlock()
return handlerIndex
}
func (pool *workerPool) releaseHandler(handlerIndex int) {
pool.mutex.Lock()
pool.availableIndices = append(pool.availableIndices, handlerIndex)
pool.mutex.Unlock()
pool.syncCond.Signal()
}
この実装から重要な洞察が得られます:
len(availableIndices) == len(taskHandlers)のとき、Poolは完全にアイドル状態- スケールアップ中に
availableIndicesの順序が崩れる(例:[1,2,3] → [1,2] → [1,2,4,5] → [1,2,4,5,3])
完全な修正
これらの問題を解決するため、AIアシスタントは以下の完全版を生成しました:
func (pool *workerPool) scalePool() {
ticker := time.NewTicker(pool.scalingDelay)
defer ticker.Stop()
for {
select {
case <-ticker.C:
pool.syncCond.L.Lock()
currentSize := len(pool.taskHandlers)
// 負荷が高い場合はスケールアップ
if len(pool.jobChannel) > currentSize*3/4 && currentSize < pool.capacity {
scaleAmount := min(currentSize*2, pool.capacity) - currentSize
for j := 0; j < scaleAmount; j++ {
handler := newTaskHandler()
pool.taskHandlers = append(pool.taskHandlers, handler)
newIndex := len(pool.taskHandlers) - 1
pool.availableIndices = append(pool.availableIndices, newIndex)
handler.launch(pool, newIndex)
}
}
// アイドル時のみスケールダウン(availableIndicesの長さもチェック)
if len(pool.jobChannel) == 0 && len(pool.availableIndices) == currentSize && currentSize > pool.baselineSize {
reductionCount := (currentSize - pool.baselineSize + 1) / 2
// スライス操作前にソートを実行
sort.Ints(pool.availableIndices)
pool.taskHandlers = pool.taskHandlers[:currentSize-reductionCount]
pool.availableIndices = pool.availableIndices[:len(pool.availableIndices)-reductionCount]
}
pool.syncCond.L.Unlock()
case <-pool.context.Done():
return
}
}
}
この修正により、最初のユーザーコードが正常に実行されるようになりました。
ドキュメントとコミットメッセージの生成
修正後、AIアシスタントにドキュメントの更新を依頼しました。変更内容をコンテキストとして提供すると、適切な説明とコード例を含むMarkdown形式のドキュメントを生成します。
また、git diffの内容をAIに送信することで、意味のあるコミットメッセージを自動生成することも可能です。例えば「fix: correct worker scaling calculation and add idle check」のような具体的なメッセージが得られます。
効果的なAI活用のポイント
正確なコンテキスト提供が鍵となります。コードベースの特定部分を選択してAIに提示し、それに基づいて質問することで、より正確で実用的な回答を得ることができます。