yao/agent/robot/manager/recovery.go
Max ce19e9bdb7 feat(robot): enhance manager and execution handling
- Added GetManager function to retrieve the global manager instance, returning nil if not started.
- Introduced ExecRecovered event to notify about recovered non-terminal executions during manager startup.
- Updated execution store to support querying by multiple statuses with ListByStatuses method.
- Enhanced integration tests to accommodate longer tick intervals and added sleep delays for stability.
- Improved cleanup logic in integration tests to prevent interference from previous execution records.
2026-03-23 17:46:32 +08:00

87 lines
2.6 KiB
Go

package manager
import (
"context"
"log"
"github.com/yaoapp/yao/agent/robot/events"
"github.com/yaoapp/yao/agent/robot/store"
"github.com/yaoapp/yao/agent/robot/types"
)
var nonTerminalStatuses = []types.ExecStatus{
types.ExecRunning, types.ExecPaused, types.ExecPending,
types.ExecWaiting, types.ExecConfirming,
}
// recoverExecutions scans the DB for non-terminal executions left by a prior
// server crash. Running/paused/pending records are marked failed; waiting/confirming
// records are kept as-is and returned for notification.
func (m *Manager) recoverExecutions(ctx context.Context) []events.ExecPayload {
execStore := store.NewExecutionStore()
robotStore := store.NewRobotStore()
var pendingNotifications []events.ExecPayload
affectedMembers := map[string]bool{}
pageSize := 100
for page := 1; ; page++ {
result, err := execStore.ListByStatuses(ctx, nonTerminalStatuses, &store.ListOptions{
Page: page,
PageSize: pageSize,
})
if err != nil {
log.Printf("[recovery] failed to list non-terminal executions page %d: %v", page, err)
break
}
if len(result.Data) == 0 {
break
}
for _, record := range result.Data {
affectedMembers[record.MemberID] = true
switch record.Status {
case types.ExecRunning, types.ExecPaused, types.ExecPending:
if err := execStore.UpdateStatus(ctx, record.ExecutionID, types.ExecFailed,
"execution interrupted by server restart"); err != nil {
log.Printf("[recovery] failed to mark %s as failed: %v", record.ExecutionID, err)
}
case types.ExecWaiting, types.ExecConfirming:
pendingNotifications = append(pendingNotifications, events.ExecPayload{
ExecutionID: record.ExecutionID,
MemberID: record.MemberID,
TeamID: record.TeamID,
Status: string(record.Status),
})
}
}
if len(result.Data) < pageSize {
break
}
}
fixRobotStatuses(ctx, execStore, robotStore, affectedMembers)
return pendingNotifications
}
// fixRobotStatuses sets robots to idle when they no longer have any non-terminal executions.
func fixRobotStatuses(ctx context.Context, execStore *store.ExecutionStore, robotStore *store.RobotStore, members map[string]bool) {
for memberID := range members {
result, err := execStore.ListByStatuses(ctx, nonTerminalStatuses, &store.ListOptions{
MemberID: memberID,
PageSize: 1,
})
if err != nil {
log.Printf("[recovery] failed to check remaining executions for %s: %v", memberID, err)
continue
}
if result.Total == 0 {
if err := robotStore.UpdateStatus(ctx, memberID, types.RobotIdle); err != nil {
log.Printf("[recovery] failed to set %s to idle: %v", memberID, err)
}
}
}
}