Fix N+1 query in convoy status fetching (gt-wah8i)

getTrackedIssues was spawning a separate bd show subprocess for each
tracked issue. With 10 convoys x 5 issues = 50+ subprocesses per poll.

Solution: Use bd show batch capability (bd show id1 id2 id3 --json)
to fetch all issue details in a single call. Falls back to individual
lookups if the batch fails (e.g., invalid IDs).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/max
2025-12-30 23:26:06 -08:00
committed by Steve Yegge
parent f3f46de20d
commit 0dc2c4ab7b

View File

@@ -551,6 +551,7 @@ type trackedIssueInfo struct {
// getTrackedIssues queries SQLite directly to get issues tracked by a convoy.
// This is needed because bd dep list doesn't properly show cross-rig external dependencies.
// Uses batched lookup to avoid N+1 subprocess calls.
func getTrackedIssues(townBeads, convoyID string) []trackedIssueInfo {
dbPath := filepath.Join(townBeads, "beads.db")
@@ -572,7 +573,9 @@ func getTrackedIssues(townBeads, convoyID string) []trackedIssueInfo {
return nil
}
var tracked []trackedIssueInfo
// First pass: collect all issue IDs (normalized from external refs)
issueIDs := make([]string, 0, len(deps))
idToDepType := make(map[string]string)
for _, dep := range deps {
issueID := dep.DependsOnID
@@ -584,14 +587,22 @@ func getTrackedIssues(townBeads, convoyID string) []trackedIssueInfo {
}
}
// Try to get issue details from the appropriate rig
issueIDs = append(issueIDs, issueID)
idToDepType[issueID] = dep.Type
}
// Single batch call to get all issue details
detailsMap := getIssueDetailsBatch(issueIDs)
// Second pass: build result using the batch lookup
var tracked []trackedIssueInfo
for _, issueID := range issueIDs {
info := trackedIssueInfo{
ID: issueID,
Type: dep.Type,
Type: idToDepType[issueID],
}
// Query issue status (try to find it in any known beads location)
if details := getIssueDetails(issueID); details != nil {
if details, ok := detailsMap[issueID]; ok {
info.Title = details.Title
info.Status = details.Status
info.IssueType = details.IssueType
@@ -614,7 +625,57 @@ type issueDetails struct {
IssueType string
}
// getIssueDetailsBatch fetches details for multiple issues in a single bd show call.
// Returns a map from issue ID to details. Missing/invalid issues are omitted from the map.
func getIssueDetailsBatch(issueIDs []string) map[string]*issueDetails {
result := make(map[string]*issueDetails)
if len(issueIDs) == 0 {
return result
}
// Build args: bd show id1 id2 id3 ... --json
args := append([]string{"show"}, issueIDs...)
args = append(args, "--json")
showCmd := exec.Command("bd", args...)
var stdout bytes.Buffer
showCmd.Stdout = &stdout
if err := showCmd.Run(); err != nil {
// Batch failed - fall back to individual lookups for robustness
// This handles cases where some IDs are invalid/missing
for _, id := range issueIDs {
if details := getIssueDetails(id); details != nil {
result[id] = details
}
}
return result
}
var issues []struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
IssueType string `json:"issue_type"`
}
if err := json.Unmarshal(stdout.Bytes(), &issues); err != nil {
return result
}
for _, issue := range issues {
result[issue.ID] = &issueDetails{
ID: issue.ID,
Title: issue.Title,
Status: issue.Status,
IssueType: issue.IssueType,
}
}
return result
}
// getIssueDetails fetches issue details by trying to show it via bd.
// Prefer getIssueDetailsBatch for multiple issues to avoid N+1 subprocess calls.
func getIssueDetails(issueID string) *issueDetails {
// Use bd show with routing - it should find the issue in the right rig
showCmd := exec.Command("bd", "show", issueID, "--json")