package store import ( "errors" "time" "git.ipao.vip/rogee/go-sip/internal/contract" "git.ipao.vip/rogee/go-sip/internal/mq" "git.ipao.vip/rogee/go-sip/internal/tenant" "github.com/google/uuid" ) // CompleteTaskControl records an observed applied revision. The caller must // validate the Agent's APPLIED receipt before invoking this transaction. func (s *Store) CompleteTaskControl(target PendingTaskControl, appliedRevision int64) error { if target.AgentID == "" || target.ExpectedRevision < 1 || appliedRevision != target.ExpectedRevision+1 { return ErrCASConflict } s.mu.Lock() defer s.mu.Unlock() tx, err := s.db.Begin() if err != nil { return err } defer tx.Rollback() var agentID string var expected int64 var already bool if err := tx.QueryRow(`SELECT a.agent_id,c.expected_revision,t.applied FROM mq_task_control_targets t JOIN mq_task_controls c ON c.tenant_id=t.tenant_id AND c.command_id=t.command_id JOIN execution_agents a ON a.execution_id=t.execution_id WHERE t.tenant_id=? AND t.command_id=? AND t.execution_id=?`, target.TenantID, target.CommandID, target.ExecutionID).Scan(&agentID, &expected, &already); err != nil { return err } if agentID != target.AgentID || expected != target.ExpectedRevision { return ErrMessageScope } if already { return nil } if _, err := tx.Exec(`UPDATE mq_task_control_targets SET applied=1 WHERE tenant_id=? AND command_id=? AND execution_id=?`, target.TenantID, target.CommandID, target.ExecutionID); err != nil { return err } var pending int if err := tx.QueryRow(`SELECT COUNT(*) FROM mq_task_control_targets WHERE tenant_id=? AND command_id=? AND applied=0`, target.TenantID, target.CommandID).Scan(&pending); err != nil { return err } if pending != 0 { return tx.Commit() } var raw []byte if err := tx.QueryRow(`SELECT body FROM inbox WHERE tenant_id=? AND command_id=?`, target.TenantID, target.CommandID).Scan(&raw); err != nil { return err } command, err := contract.DecodeMQCommand(raw) if err != nil { return err } route, err := tenant.NewDispatcherRoute(command.DispatcherID, command.TenantKey) if err != nil { return err } if err := bindMQScope(tx, command.DispatcherID, command.TenantID, command.TenantKey); err != nil { return err } var taskID, action string if err := tx.QueryRow(`SELECT task_id,action FROM mq_task_controls WHERE tenant_id=? AND command_id=?`, target.TenantID, target.CommandID).Scan(&taskID, &action); err != nil { return err } var total, matches int if err := tx.QueryRow(`SELECT COUNT(*),COALESCE(SUM(tasks.task_revision=?),0) FROM tasks JOIN mq_task_control_targets t ON t.execution_id=tasks.execution_id WHERE t.tenant_id=? AND t.command_id=?`, expected, target.TenantID, target.CommandID).Scan(&total, &matches); err != nil { return err } if total == 0 || matches != total { return ErrCASConflict } now := s.now().UTC() if _, err := tx.Exec(`UPDATE tasks SET task_revision=?,updated_at=?,status=CASE WHEN ?='resume' AND EXISTS (SELECT 1 FROM reservations r WHERE r.execution_id=tasks.execution_id AND r.state IN ('held','unknown')) THEN 'running' ELSE status END WHERE execution_id IN (SELECT execution_id FROM mq_task_control_targets WHERE tenant_id=? AND command_id=?)`, appliedRevision, now.Format(time.RFC3339Nano), action, target.TenantID, target.CommandID); err != nil { return err } eventID := uuid.NewSHA1(uuid.NameSpaceURL, []byte("task.control.applied:"+command.DispatcherID+":"+command.TenantID+":"+command.CommandID)).String() body, err := (contract.EventBuilder{TenantID: command.TenantID, TenantKey: command.TenantKey, TraceID: command.TraceID, EventType: "command.result", Aggregate: "command", AggregateID: command.CommandID, Version: 2, Payload: map[string]any{"command_id": command.CommandID, "command_type": "task.control", "task_id": taskID, "status": "applied", "reason_code": "applied", "requested_task_revision": expected, "applied_task_revision": appliedRevision}}).MarshalMQ(command.DispatcherID, now, eventID) if err != nil { return err } if _, err := tx.Exec(`INSERT INTO outbox(event_id,tenant_key,exchange,routing_key,body,status,created_at) VALUES(?,?,?,?,?,'pending',?)`, eventID, command.TenantKey, mq.EventExchange, route.OutboundKey, body, now.Format(time.RFC3339Nano)); err != nil { return err } if _, err := tx.Exec(`UPDATE mq_task_controls SET state='applied' WHERE tenant_id=? AND command_id=?`, target.TenantID, target.CommandID); err != nil { return err } result, err := tx.Exec(`UPDATE mq_command_receipts SET response_id=? WHERE tenant_id=? AND command_id=?`, eventID, target.TenantID, target.CommandID) if err != nil { return err } count, err := result.RowsAffected() if err != nil { return err } if count != 1 { return errors.New("control receipt link missing") } return tx.Commit() }