165 lines
7.7 KiB
Go
165 lines
7.7 KiB
Go
package store
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
agentpb "git.ipao.vip/rogee/go-sip/gen/agent"
|
|
)
|
|
|
|
var ErrLocalOriginationNotAuthorized = errors.New("local execution is not authorized for origination")
|
|
|
|
type LocalOriginationCandidate struct {
|
|
DispatcherID string
|
|
ExecutionID string
|
|
TenantID string
|
|
TenantKey string
|
|
TaskID string
|
|
CommandID string
|
|
Callee string
|
|
CommandBody json.RawMessage
|
|
Snapshot LocalExecutionConfigSnapshot
|
|
SnapshotSHA256 string
|
|
}
|
|
|
|
// LoadLocalOrigination checks current task, reservation, control, discovery and
|
|
// assignment state. Its caller must still make the final time/policy decision
|
|
// against the immutable bound snapshot and atomically claim the task below.
|
|
func (s *Store) LoadLocalOrigination(dispatcherID, executionID string) (LocalOriginationCandidate, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if dispatcherID == "" || executionID == "" {
|
|
return LocalOriginationCandidate{}, ErrLocalOriginationNotAuthorized
|
|
}
|
|
candidate := LocalOriginationCandidate{DispatcherID: dispatcherID, ExecutionID: executionID}
|
|
var taskStatus, inboxStatus, inboxHash, reservationState, admissionState string
|
|
var removed, ready int
|
|
var snapshotBody []byte
|
|
err := s.db.QueryRow(`SELECT t.tenant_id,t.tenant_key,t.task_id,t.task_item_id,t.callee,t.status,
|
|
i.body,i.body_hash,i.status,c.body,c.content_sha256,r.state,a.admission_state,a.removed,d.ready
|
|
FROM tasks t
|
|
JOIN inbox i ON i.command_id=t.task_item_id AND i.command_type='call.execute'
|
|
JOIN local_v01_execution_configs c ON c.execution_id=t.execution_id
|
|
JOIN reservations r ON r.execution_id=t.execution_id
|
|
JOIN local_v01_task_assignments a ON a.dispatcher_id=? AND a.task_id=t.task_id
|
|
JOIN local_v04_task_discovery_state d ON d.dispatcher_id=a.dispatcher_id
|
|
WHERE t.execution_id=?`, dispatcherID, executionID).Scan(&candidate.TenantID, &candidate.TenantKey, &candidate.TaskID, &candidate.CommandID, &candidate.Callee, &taskStatus,
|
|
&candidate.CommandBody, &inboxHash, &inboxStatus, &snapshotBody, &candidate.SnapshotSHA256, &reservationState, &admissionState, &removed, &ready)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return LocalOriginationCandidate{}, ErrLocalOriginationNotAuthorized
|
|
}
|
|
if err != nil {
|
|
return LocalOriginationCandidate{}, fmt.Errorf("load local origination facts: %w", err)
|
|
}
|
|
if taskStatus != "reserved" || inboxStatus != "persisted" || reservationState != "held" || admissionState != "running" || removed != 0 || ready != 1 {
|
|
return LocalOriginationCandidate{}, fmt.Errorf("%w: task=%s inbox=%s reservation=%s admission=%s removed=%d discovery_ready=%d", ErrLocalOriginationNotAuthorized,
|
|
taskStatus, inboxStatus, reservationState, admissionState, removed, ready)
|
|
}
|
|
if err := validateLocalOriginationCandidate(&candidate, snapshotBody, inboxHash); err != nil {
|
|
return LocalOriginationCandidate{}, err
|
|
}
|
|
return candidate, nil
|
|
}
|
|
|
|
func validateLocalOriginationCandidate(candidate *LocalOriginationCandidate, snapshotBody []byte, inboxHash string) error {
|
|
commandDigest := sha256.Sum256(candidate.CommandBody)
|
|
snapshotDigest := sha256.Sum256(snapshotBody)
|
|
if hex.EncodeToString(commandDigest[:]) != inboxHash || hex.EncodeToString(snapshotDigest[:]) != candidate.SnapshotSHA256 {
|
|
return fmt.Errorf("%w: persisted command or snapshot digest mismatch", ErrLocalOriginationNotAuthorized)
|
|
}
|
|
if err := json.Unmarshal(snapshotBody, &candidate.Snapshot); err != nil {
|
|
return fmt.Errorf("decode bound execution snapshot: %w", err)
|
|
}
|
|
if candidate.Snapshot.SchemaVersion != "execution-config-snapshot.v0.3" || candidate.Snapshot.DispatcherID != candidate.DispatcherID ||
|
|
candidate.Snapshot.TaskID != candidate.TaskID || candidate.Snapshot.TenantID != candidate.TenantID || candidate.Snapshot.TenantKey != candidate.TenantKey ||
|
|
candidate.Snapshot.SelectedTrunkID == "" {
|
|
return fmt.Errorf("%w: execution snapshot identity or selected trunk mismatch", ErrLocalOriginationNotAuthorized)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ClaimLocalOrigination commits the one-shot dial decision and its Agent
|
|
// control route together. A crash between them must not leave an unaddressable
|
|
// in-flight call. The admission reservation is not debited a second time.
|
|
func (s *Store) ClaimLocalOrigination(dispatcherID, executionID, agentID string, binding *agentpb.ExecutionBinding, decidedAt time.Time) error {
|
|
if decidedAt.IsZero() || binding == nil || binding.ExecutionId != executionID || binding.AttemptId != executionID || agentID == "" {
|
|
return ErrLocalOriginationNotAuthorized
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
if err := bindExecutionAgentTx(tx, agentID, binding); err != nil {
|
|
return fmt.Errorf("bind local origination Agent: %w", err)
|
|
}
|
|
result, err := tx.Exec(`INSERT INTO local_v02_origination_decisions(execution_id,dispatcher_id,decision,reason,decided_at)
|
|
SELECT t.execution_id,?,'issued','',? FROM tasks t WHERE t.execution_id=? AND t.status='reserved'
|
|
AND EXISTS (SELECT 1 FROM reservations r WHERE r.execution_id=t.execution_id AND r.state='held')
|
|
AND EXISTS (SELECT 1 FROM local_v01_task_assignments a WHERE a.dispatcher_id=? AND a.task_id=t.task_id AND a.tenant_id=t.tenant_id AND a.tenant_key=t.tenant_key AND a.removed=0 AND a.admission_state='running')
|
|
AND EXISTS (SELECT 1 FROM local_v04_task_discovery_state d WHERE d.dispatcher_id=? AND d.ready=1)
|
|
AND NOT EXISTS (SELECT 1 FROM controls c WHERE c.execution_id=t.execution_id AND c.action IN ('pause','drain','stop','hangup'))
|
|
AND NOT EXISTS (SELECT 1 FROM local_v02_origination_decisions old WHERE old.execution_id=t.execution_id)`,
|
|
dispatcherID, decidedAt.UTC().Format(time.RFC3339Nano), executionID, dispatcherID, dispatcherID)
|
|
if err != nil {
|
|
return fmt.Errorf("claim local origination: %w", err)
|
|
}
|
|
count, err := result.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count != 1 {
|
|
return ErrLocalOriginationNotAuthorized
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// RefuseLocalOrigination records a failed final gate, not just a transient
|
|
// return value. A duplicate command or a clock change cannot revive it. It
|
|
// never changes an already issued decision.
|
|
func (s *Store) RefuseLocalOrigination(dispatcherID, executionID, reason string, decidedAt time.Time) error {
|
|
switch reason {
|
|
case "policy_denied", "control_closed", "invalid_binding":
|
|
default:
|
|
return fmt.Errorf("invalid local origination refusal reason %q", reason)
|
|
}
|
|
if dispatcherID == "" || executionID == "" || decidedAt.IsZero() {
|
|
return ErrLocalOriginationNotAuthorized
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
result, err := s.db.Exec(`INSERT INTO local_v02_origination_decisions(execution_id,dispatcher_id,decision,reason,decided_at)
|
|
SELECT t.execution_id,?,'refused',?,? FROM tasks t WHERE t.execution_id=? AND t.status='reserved'
|
|
AND EXISTS (SELECT 1 FROM local_v01_task_assignments a WHERE a.dispatcher_id=? AND a.task_id=t.task_id AND a.tenant_id=t.tenant_id AND a.tenant_key=t.tenant_key)
|
|
AND NOT EXISTS (SELECT 1 FROM local_v02_origination_decisions old WHERE old.execution_id=t.execution_id)`,
|
|
dispatcherID, reason, decidedAt.UTC().Format(time.RFC3339Nano), executionID, dispatcherID)
|
|
if err != nil {
|
|
return fmt.Errorf("persist local origination refusal: %w", err)
|
|
}
|
|
count, err := result.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count == 1 {
|
|
return nil
|
|
}
|
|
var previous string
|
|
if err := s.db.QueryRow(`SELECT decision FROM local_v02_origination_decisions WHERE execution_id=? AND dispatcher_id=?`, executionID, dispatcherID).Scan(&previous); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return ErrLocalOriginationNotAuthorized
|
|
}
|
|
return fmt.Errorf("read local origination decision: %w", err)
|
|
}
|
|
if previous == "refused" {
|
|
return nil
|
|
}
|
|
return ErrLocalOriginationNotAuthorized
|
|
}
|