Files

107 lines
4.7 KiB
Go

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