Go言語ワーカープールの動的スケーリングバグをAIアシスタントで解決する

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()
}

この実装から重要な洞察が得られます:

  1. len(availableIndices) == len(taskHandlers)のとき、Poolは完全にアイドル状態
  2. スケールアップ中に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に提示し、それに基づいて質問することで、より正確で実用的な回答を得ることができます。

タグ: Go GoPool 並行処理 バグ修正 ワーカープール

Sun, 10 May 2026 09:17:22 +0900 投稿