84 lines
3.1 KiB
Go
84 lines
3.1 KiB
Go
package dispatcher
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
agentv1 "git.ipao.vip/rogee/go-sip/gen/agent/v1"
|
|
)
|
|
|
|
type TaskController interface {
|
|
Control(context.Context, string, *agentv1.ExecutionBinding, agentv1.ControlAction, agentv1.ActiveCallPolicy) (*agentv1.ApplyTaskControlResponse, error)
|
|
}
|
|
|
|
// ProcessTaskControls retries only the original control, never an execution or
|
|
// originate. Uncertain acknowledgements leave the durable target pending.
|
|
func (d *Dispatcher) ProcessTaskControls(ctx context.Context, controller TaskController, limit int) (int, error) {
|
|
if controller == nil {
|
|
return 0, errors.New("task controller is required")
|
|
}
|
|
ctx, cancelBatch := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancelBatch()
|
|
// ponytail: one Agent per P1 Dispatcher; serialization orders control against
|
|
// last execution admission. Revisit granularity only if this scope expands.
|
|
d.executionMu.Lock()
|
|
defer d.executionMu.Unlock()
|
|
targets, err := d.store.PendingTaskControls(limit)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
completed := 0
|
|
var failures []error
|
|
for _, target := range targets {
|
|
if err := ctx.Err(); err != nil {
|
|
return completed, errors.Join(append(failures, err)...)
|
|
}
|
|
if target.Binding == nil || target.AgentID == "" {
|
|
failures = append(failures, fmt.Errorf("control %s execution %s has no durable Agent assignment", target.CommandID, target.ExecutionID))
|
|
continue
|
|
}
|
|
var action agentv1.ControlAction
|
|
switch target.Action {
|
|
case "pause":
|
|
action = agentv1.ControlAction_CONTROL_ACTION_PAUSE
|
|
case "resume":
|
|
action = agentv1.ControlAction_CONTROL_ACTION_RESUME
|
|
case "stop":
|
|
action = agentv1.ControlAction_CONTROL_ACTION_STOP
|
|
default:
|
|
failures = append(failures, errors.New("invalid persisted control action"))
|
|
continue
|
|
}
|
|
var policy agentv1.ActiveCallPolicy
|
|
switch target.Policy {
|
|
case "drain":
|
|
policy = agentv1.ActiveCallPolicy_ACTIVE_CALL_POLICY_DRAIN
|
|
case "hangup":
|
|
policy = agentv1.ActiveCallPolicy_ACTIVE_CALL_POLICY_HANGUP
|
|
default:
|
|
failures = append(failures, errors.New("invalid persisted active-call policy"))
|
|
continue
|
|
}
|
|
attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
response, err := controller.Control(attemptCtx, target.AgentID, target.Binding, action, policy)
|
|
cancel()
|
|
if err == nil && (response.GetReceipt().GetResult() != agentv1.ResultCode_RESULT_CODE_APPLIED || response.GetAppliedTaskRevision() != target.ExpectedRevision+1) {
|
|
err = errors.New("Agent has not confirmed the requested applied revision")
|
|
}
|
|
if err == nil {
|
|
err = d.store.CompleteTaskControl(target, response.AppliedTaskRevision)
|
|
}
|
|
if err != nil {
|
|
slog.Error("task control remains pending", "command_id", target.CommandID, "execution_id", target.ExecutionID, "error", err)
|
|
failures = append(failures, fmt.Errorf("control %s: %w", target.CommandID, err))
|
|
continue
|
|
}
|
|
slog.Info("Agent task control application persisted", "command_id", target.CommandID, "execution_id", target.ExecutionID, "revision", response.AppliedTaskRevision)
|
|
completed++
|
|
}
|
|
return completed, errors.Join(failures...)
|
|
}
|