178 lines
6.0 KiB
Go
178 lines
6.0 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// CloseLocalTaskDiscoveryAdmissionV04 fails closed without persisting a live cursor.
|
|
func (s *Store) CloseLocalTaskDiscoveryAdmissionV04(dispatcherID string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
_, err := s.db.Exec(`INSERT INTO local_v04_task_discovery_state(dispatcher_id,ready,updated_at) VALUES(?,0,?)
|
|
ON CONFLICT(dispatcher_id) DO UPDATE SET ready=0,updated_at=excluded.updated_at`, dispatcherID, time.Now().UTC().Format(time.RFC3339Nano))
|
|
return err
|
|
}
|
|
|
|
// LocalTaskDiscoveryReadyV04 reports whether a complete snapshot and control
|
|
// backlog have been applied. An absent row is not ready on a fresh host.
|
|
func (s *Store) LocalTaskDiscoveryReadyV04(dispatcherID string) (bool, error) {
|
|
if dispatcherID == "" {
|
|
return false, ErrLocalDiscoveryUnavailable
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
var ready int
|
|
if err := s.db.QueryRow(`SELECT ready FROM local_v04_task_discovery_state WHERE dispatcher_id=?`, dispatcherID).Scan(&ready); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
return ready == 1, nil
|
|
}
|
|
|
|
// MarkLocalTaskDiscoveryReadyV04 opens admission after the live change stream is caught up.
|
|
func (s *Store) MarkLocalTaskDiscoveryReadyV04(dispatcherID string, observedAt time.Time) error {
|
|
if dispatcherID == "" || observedAt.IsZero() {
|
|
return ErrLocalDiscoveryUnavailable
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
result, err := s.db.Exec(`UPDATE local_v04_task_discovery_state SET ready=1,updated_at=? WHERE dispatcher_id=?`, observedAt.UTC().Format(time.RFC3339Nano), dispatcherID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n, err := result.RowsAffected(); err != nil || n != 1 {
|
|
return ErrLocalDiscoveryUnavailable
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ApplyLocalTaskSnapshot replaces membership only after every page of one snapshot
|
|
// has been verified by the caller. Control barriers and execution records survive.
|
|
func (s *Store) ApplyLocalTaskSnapshot(dispatcherID string, tasks []LocalDiscoveredTask, observedAt time.Time) error {
|
|
if dispatcherID == "" || observedAt.IsZero() || len(tasks) > 256 {
|
|
return ErrLocalDiscoveryUnavailable
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if err := validateV04AssignmentSet(dispatcherID, tasks); err != nil {
|
|
return err
|
|
}
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
now := observedAt.UTC().Format(time.RFC3339Nano)
|
|
seen := make(map[string]bool, len(tasks))
|
|
for _, task := range tasks {
|
|
if task.Status == "removed" {
|
|
return ErrLocalDiscoveryUnavailable
|
|
}
|
|
seen[task.TaskID] = true
|
|
if err := applyLocalDiscoveredTask(tx, dispatcherID, task, now); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
rows, err := tx.Query(`SELECT task_id FROM local_v01_task_assignments WHERE dispatcher_id=? AND removed=0`, dispatcherID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var absent []string
|
|
for rows.Next() {
|
|
var taskID string
|
|
if err := rows.Scan(&taskID); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
if !seen[taskID] {
|
|
absent = append(absent, taskID)
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
rows.Close()
|
|
for _, taskID := range absent {
|
|
if _, err := tx.Exec(`UPDATE local_v01_task_assignments SET saas_status='removed',admission_state='removed',removed=1,updated_at=? WHERE dispatcher_id=? AND task_id=?`, now, dispatcherID, taskID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, err := tx.Exec(`INSERT INTO local_v04_task_discovery_state(dispatcher_id,ready,updated_at) VALUES(?,0,?)
|
|
ON CONFLICT(dispatcher_id) DO UPDATE SET ready=0,updated_at=excluded.updated_at`, dispatcherID, now); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// ApplyLocalTaskChanges updates task membership. An existing task's status is
|
|
// controlled by MQ; contradictory HTTP status fails admission instead of racing it.
|
|
func (s *Store) ApplyLocalTaskChanges(dispatcherID string, tasks []LocalDiscoveredTask, observedAt time.Time) error {
|
|
if dispatcherID == "" || observedAt.IsZero() || len(tasks) > 256 {
|
|
return ErrLocalDiscoveryUnavailable
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if err := validateV04AssignmentSet(dispatcherID, tasks); err != nil {
|
|
return err
|
|
}
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
for _, task := range tasks {
|
|
var existingStatus string
|
|
err := tx.QueryRow(`SELECT saas_status FROM local_v01_task_assignments WHERE dispatcher_id=? AND task_id=?`, dispatcherID, task.TaskID).Scan(&existingStatus)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
if err == nil && task.Status != "removed" && task.Status != existingStatus {
|
|
if err := tx.Rollback(); err != nil {
|
|
return err
|
|
}
|
|
if _, err := s.db.Exec(`UPDATE local_v04_task_discovery_state SET ready=0 WHERE dispatcher_id=?`, dispatcherID); err != nil {
|
|
return err
|
|
}
|
|
return fmt.Errorf("task %s: HTTP status %s conflicts with MQ-applied %s: %w", task.TaskID, task.Status, existingStatus, ErrLocalDiscoveryUnavailable)
|
|
}
|
|
}
|
|
now := observedAt.UTC().Format(time.RFC3339Nano)
|
|
for _, task := range tasks {
|
|
if err := applyLocalDiscoveredTask(tx, dispatcherID, task, now); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, err := tx.Exec(`UPDATE local_v04_task_discovery_state SET updated_at=? WHERE dispatcher_id=?`, now, dispatcherID); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func validateV04AssignmentSet(dispatcherID string, tasks []LocalDiscoveredTask) error {
|
|
seen := make(map[string]bool, len(tasks))
|
|
ids, keys := make(map[string]string), make(map[string]string)
|
|
for _, task := range tasks {
|
|
if err := validateLocalDiscoveredTask(dispatcherID, task); err != nil {
|
|
return err
|
|
}
|
|
if seen[task.TaskID] {
|
|
return fmt.Errorf("duplicate discovered task %s: %w", task.TaskID, ErrLocalDiscoveryUnavailable)
|
|
}
|
|
seen[task.TaskID] = true
|
|
if key, ok := ids[task.TenantID]; ok && key != task.TenantKey {
|
|
return ErrTenantBindingConflict
|
|
}
|
|
if id, ok := keys[task.TenantKey]; ok && id != task.TenantID {
|
|
return ErrTenantBindingConflict
|
|
}
|
|
ids[task.TenantID], keys[task.TenantKey] = task.TenantKey, task.TenantID
|
|
}
|
|
return nil
|
|
}
|