refactor: remove unused v0.3 task discovery runtime

This commit is contained in:
2026-09-28 07:17:54 +08:00
parent 3ea5f34583
commit ce5e8da2ee
11 changed files with 71 additions and 613 deletions
+7 -40
View File
@@ -10,14 +10,13 @@ import (
)
var (
ErrLocalDiscoveryCursorMismatch = errors.New("local task-discovery cursor mismatch")
ErrLocalDiscoveryUnavailable = errors.New("task discovery has no fresh complete response")
ErrLocalDiscoveryTaskLimit = errors.New("task discovery exceeds the 256-task per-Dispatcher limit")
ErrLocalDiscoveryTaskMissing = errors.New("local task-discovery update targets an unknown task")
ErrLocalTaskRemoved = errors.New("local task assignment has been removed")
ErrLocalTaskUnassigned = errors.New("task is not assigned to this Dispatcher")
ErrLocalTaskPaused = errors.New("task admission is paused")
ErrLocalTaskStopped = errors.New("task admission is terminal")
ErrLocalDiscoveryUnavailable = errors.New("task discovery has no fresh complete response")
ErrLocalDiscoveryTaskLimit = errors.New("task discovery exceeds the 256-task per-Dispatcher limit")
ErrLocalDiscoveryTaskMissing = errors.New("local task-discovery update targets an unknown task")
ErrLocalTaskRemoved = errors.New("local task assignment has been removed")
ErrLocalTaskUnassigned = errors.New("task is not assigned to this Dispatcher")
ErrLocalTaskPaused = errors.New("task admission is paused")
ErrLocalTaskStopped = errors.New("task admission is terminal")
)
type LocalDiscoveredTask struct {
@@ -152,38 +151,6 @@ func mergeLocalTaskState(current, incoming string) string {
}
}
// CloseLocalTaskDiscoveryAdmission rejects new work without losing the cursor
// or changing an authoritative task pause/stop state.
func (s *Store) CloseLocalTaskDiscoveryAdmission(dispatcherID string) error {
s.mu.Lock()
defer s.mu.Unlock()
_, err := s.db.Exec(`UPDATE local_v03_task_discovery_state SET ready=0 WHERE dispatcher_id=?`, dispatcherID)
return err
}
func (s *Store) LocalTaskDiscoveryCursor(dispatcherID string) (cursor string, exists bool, err error) {
s.mu.Lock()
defer s.mu.Unlock()
err = s.db.QueryRow(`SELECT cursor FROM local_v03_task_discovery_state WHERE dispatcher_id=?`, dispatcherID).Scan(&cursor)
if errors.Is(err, sql.ErrNoRows) {
var legacy, assignments int
if err := s.db.QueryRow(`SELECT COUNT(*) FROM local_v02_task_discovery_state WHERE dispatcher_id=?`, dispatcherID).Scan(&legacy); err != nil {
return "", false, err
}
if err := s.db.QueryRow(`SELECT COUNT(*) FROM local_v01_task_assignments WHERE dispatcher_id=?`, dispatcherID).Scan(&assignments); err != nil {
return "", false, err
}
if legacy != 0 || assignments != 0 {
return "", false, fmt.Errorf("%w: existing discovery state requires controlled drain before v0.3", ErrLocalDiscoveryUnavailable)
}
return "", false, nil
}
if err != nil {
return "", false, err
}
return cursor, true, nil
}
func (s *Store) LocalTaskAssignments(dispatcherID string) ([]LocalTaskAssignment, error) {
s.mu.Lock()
defer s.mu.Unlock()
-165
View File
@@ -1,165 +0,0 @@
package store
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
)
const localEventPageLimit = 256
// LocalTaskDiscoveryPage is one complete HTTP response, not an entire task list.
// SaaS guarantees response ordering/completeness; its response-level cursor is
// the only event position supplied by the project-local v0.3 proposal.
type LocalTaskDiscoveryPage struct {
DispatcherID string
FromCursor string
NextCursor string
Tasks []LocalDiscoveredTask
Body json.RawMessage
ObservedAt time.Time
}
func eventCursorValue(cursor string) (uint64, error) {
value, err := strconv.ParseUint(cursor, 10, 64)
if err != nil || strconv.FormatUint(value, 10) != cursor {
return 0, errors.New("event cursor must be a canonical decimal uint64")
}
return value, nil
}
// ApplyLocalTaskDiscoveryPage commits assignments, the exact page evidence and
// its cursor together. A nonempty page never opens new execution admission;
// only an empty final page marks the initial catch-up/current poll complete.
func (s *Store) ApplyLocalTaskDiscoveryPage(page LocalTaskDiscoveryPage) error {
from, err := eventCursorValue(page.FromCursor)
if err != nil {
return fmt.Errorf("invalid task-discovery after cursor: %w", err)
}
to, err := eventCursorValue(page.NextCursor)
if err != nil {
return fmt.Errorf("invalid task-discovery next cursor: %w", err)
}
if page.DispatcherID == "" || page.ObservedAt.IsZero() || len(page.Tasks) > localEventPageLimit || !json.Valid(page.Body) ||
(len(page.Tasks) == 0 && to != from) || (len(page.Tasks) != 0 && to <= from) {
return ErrLocalDiscoveryUnavailable
}
seen := make(map[string]struct{}, len(page.Tasks))
for _, task := range page.Tasks {
if err := validateLocalDiscoveredTask(page.DispatcherID, task); err != nil {
return err
}
if _, duplicate := seen[task.TaskID]; duplicate {
return fmt.Errorf("task-discovery page has duplicate task ID %q", task.TaskID)
}
seen[task.TaskID] = struct{}{}
}
s.mu.Lock()
defer s.mu.Unlock()
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
var legacy int
if err := tx.QueryRow(`SELECT COUNT(*) FROM local_v02_task_discovery_state WHERE dispatcher_id=?`, page.DispatcherID).Scan(&legacy); err != nil {
return err
}
if legacy != 0 {
return fmt.Errorf("%w: v0.2 discovery checkpoint must be drained before switching", ErrLocalDiscoveryUnavailable)
}
var cursor string
err = tx.QueryRow(`SELECT cursor FROM local_v03_task_discovery_state WHERE dispatcher_id=?`, page.DispatcherID).Scan(&cursor)
if errors.Is(err, sql.ErrNoRows) {
var existing int
if err := tx.QueryRow(`SELECT COUNT(*) FROM local_v01_task_assignments WHERE dispatcher_id=?`, page.DispatcherID).Scan(&existing); err != nil {
return err
}
if existing != 0 {
return fmt.Errorf("%w: existing assignments have no v0.3 checkpoint", ErrLocalDiscoveryUnavailable)
}
if page.FromCursor != "0" {
return ErrLocalDiscoveryCursorMismatch
}
} else if err != nil {
return err
} else if cursor != page.FromCursor {
return ErrLocalDiscoveryCursorMismatch
}
now := page.ObservedAt.UTC().Format(time.RFC3339Nano)
for _, task := range page.Tasks {
if err := bindLocalTenant(tx, page.DispatcherID, task.TenantID, task.TenantKey, page.ObservedAt); err != nil {
return err
}
if err := applyLocalDiscoveredTask(tx, page.DispatcherID, task, now); err != nil {
return err
}
}
var active int
if err := tx.QueryRow(`SELECT COUNT(*) FROM local_v01_task_assignments WHERE dispatcher_id=? AND removed=0`, page.DispatcherID).Scan(&active); err != nil {
return err
}
if active > localEventPageLimit {
return ErrLocalDiscoveryTaskLimit
}
ready := 0
if len(page.Tasks) == 0 {
ready = 1
}
sum := sha256.Sum256(page.Body)
_, err = tx.Exec(`INSERT INTO local_v03_task_discovery_state(dispatcher_id,cursor,ready,body,content_sha256,updated_at)
VALUES(?,?,?,?,?,?) ON CONFLICT(dispatcher_id) DO UPDATE SET cursor=excluded.cursor,ready=excluded.ready,
body=excluded.body,content_sha256=excluded.content_sha256,updated_at=excluded.updated_at`,
page.DispatcherID, page.NextCursor, ready, []byte(page.Body), hex.EncodeToString(sum[:]), now)
if err != nil {
return err
}
return tx.Commit()
}
// LocalDiscoveredTaskForConfig binds a config read to a fully caught-up,
// durable task assignment. A partial page cannot authorize configuration.
func (s *Store) LocalDiscoveredTaskForConfig(dispatcherID, taskID, tenantID string) (LocalTaskAssignment, error) {
s.mu.Lock()
defer s.mu.Unlock()
var ready int
var observedAt string
err := s.db.QueryRow(`SELECT ready,updated_at FROM local_v04_task_discovery_state WHERE dispatcher_id=?`, dispatcherID).Scan(&ready, &observedAt)
if errors.Is(err, sql.ErrNoRows) || (err == nil && ready != 1) {
return LocalTaskAssignment{}, ErrLocalDiscoveryUnavailable
}
if err != nil {
return LocalTaskAssignment{}, err
}
var assignment LocalTaskAssignment
var removed int
err = s.db.QueryRow(`SELECT dispatcher_id,task_id,tenant_id,tenant_key,task_revision,saas_status,admission_state,removed,
queue_exchange,routing_key,binding_key,queue_name,updated_at FROM local_v01_task_assignments WHERE dispatcher_id=? AND task_id=?`,
dispatcherID, taskID).Scan(&assignment.DispatcherID, &assignment.TaskID, &assignment.TenantID, &assignment.TenantKey,
&assignment.TaskRevision, &assignment.Status, &assignment.AdmissionState, &removed, &assignment.Queue.Exchange,
&assignment.Queue.RoutingKey, &assignment.Queue.BindingKey, &assignment.Queue.QueueName, &assignment.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
return LocalTaskAssignment{}, ErrLocalDiscoveryTaskMissing
}
if err != nil {
return LocalTaskAssignment{}, err
}
if assignment.TenantID != tenantID {
return LocalTaskAssignment{}, ErrTenantBindingConflict
}
assignment.Removed = removed != 0
if assignment.Removed {
return LocalTaskAssignment{}, ErrLocalTaskRemoved
}
assignment.DiscoveryObservedAt, err = time.Parse(time.RFC3339Nano, observedAt)
if err != nil {
return LocalTaskAssignment{}, fmt.Errorf("invalid durable discovery observation time: %w", err)
}
return assignment, nil
}
-118
View File
@@ -1,118 +0,0 @@
package store
import (
"crypto/sha256"
"encoding/hex"
"errors"
"testing"
"time"
)
func eventPage(after, next string, tasks ...LocalDiscoveredTask) LocalTaskDiscoveryPage {
return LocalTaskDiscoveryPage{
DispatcherID: "d-1", FromCursor: after, NextCursor: next,
Tasks: tasks, Body: []byte(`{"schema_version":"task-discovery.v0.3-proposal","tasks":[]}`),
ObservedAt: time.Now().UTC(),
}
}
func eventTask(id, status string, revision int64) LocalDiscoveredTask {
return LocalDiscoveredTask{TaskID: id, TenantID: "tenant-a", TenantKey: "tenant-key-a", Status: status, TaskRevision: revision}
}
func TestEventPagesPersistCursorAssignmentAndBootGateTogether(t *testing.T) {
st := openLocalDiscoveryTestStore(t)
first := eventPage("0", "1", eventTask("task-a", "running", 1))
if err := st.ApplyLocalTaskDiscoveryPage(first); err != nil {
t.Fatal(err)
}
cursor, exists, err := st.LocalTaskDiscoveryCursor("d-1")
if err != nil || !exists || cursor != "1" {
t.Fatalf("cursor=%q exists=%v err=%v", cursor, exists, err)
}
var ready int
var saved []byte
var digest string
if err := st.db.QueryRow(`SELECT ready,body,content_sha256 FROM local_v03_task_discovery_state WHERE dispatcher_id=?`, "d-1").Scan(&ready, &saved, &digest); err != nil {
t.Fatal(err)
}
sum := sha256.Sum256(first.Body)
if ready != 0 || string(saved) != string(first.Body) || digest != hex.EncodeToString(sum[:]) {
t.Fatalf("partial page admitted or changed evidence: ready=%d body=%s hash=%s", ready, saved, digest)
}
assignments, err := st.LocalTaskAssignments("d-1")
if err != nil || len(assignments) != 1 || assignments[0].Status != "running" || assignments[0].Queue.QueueName != "agent-call.d.d-1.task.task-a.v3" {
t.Fatalf("assignment=%+v err=%v", assignments, err)
}
if err := st.ApplyLocalTaskDiscoveryPage(eventPage("1", "1")); err != nil {
t.Fatal(err)
}
if err := st.db.QueryRow(`SELECT ready FROM local_v03_task_discovery_state WHERE dispatcher_id=?`, "d-1").Scan(&ready); err != nil || ready != 1 {
t.Fatalf("completed page ready=%d err=%v", ready, err)
}
}
func TestEventPageAllowsSameTaskNewStatusAndRemovedTombstone(t *testing.T) {
st := openLocalDiscoveryTestStore(t)
for _, page := range []LocalTaskDiscoveryPage{
eventPage("0", "1", eventTask("task-a", "running", 1)),
eventPage("1", "2", eventTask("task-a", "paused", 1)),
eventPage("2", "3", eventTask("task-a", "removed", 1)),
eventPage("3", "3"),
} {
if err := st.ApplyLocalTaskDiscoveryPage(page); err != nil {
t.Fatal(err)
}
}
assignments, err := st.LocalTaskAssignments("d-1")
if err != nil || len(assignments) != 1 || !assignments[0].Removed || assignments[0].Status != "removed" || assignments[0].AdmissionState != "removed" {
t.Fatalf("removed task=%+v err=%v", assignments, err)
}
cursor, exists, err := st.LocalTaskDiscoveryCursor("d-1")
if err != nil || !exists || cursor != "3" {
t.Fatalf("cursor=%q exists=%v err=%v", cursor, exists, err)
}
if err := st.ApplyLocalTaskDiscoveryPage(eventPage("1", "4", eventTask("task-a", "running", 1))); !errors.Is(err, ErrLocalDiscoveryCursorMismatch) {
t.Fatalf("old cursor unexpectedly accepted: %v", err)
}
}
func TestEventPageRunningAfterDiscoveryPauseWaitsForMQResume(t *testing.T) {
st := openLocalDiscoveryTestStore(t)
for _, page := range []LocalTaskDiscoveryPage{
eventPage("0", "1", eventTask("task-a", "paused", 1)),
eventPage("1", "1"),
eventPage("1", "2", eventTask("task-a", "running", 2)),
eventPage("2", "2"),
} {
if err := st.ApplyLocalTaskDiscoveryPage(page); err != nil {
t.Fatal(err)
}
}
assignment, err := st.LocalTaskAssignment("d-1", "task-a")
if err != nil || assignment.Status != "running" || assignment.TaskRevision != 2 || assignment.AdmissionState != "paused" {
t.Fatalf("discovery running incorrectly reopened admission: %+v err=%v", assignment, err)
}
cursor, exists, err := st.LocalTaskDiscoveryCursor("d-1")
if err != nil || !exists || cursor != "2" {
t.Fatalf("latest task status did not advance cursor: %q exists=%v err=%v", cursor, exists, err)
}
assignment, err = st.ResumeLocalTaskAdmission("d-1", "task-a", "tenant-a", "tenant-key-a", "running", 2)
if err != nil || assignment.Status != "running" || assignment.AdmissionState != "running" {
t.Fatalf("fresh MQ resume did not reopen admission: %+v err=%v", assignment, err)
}
}
func TestEventPageCannotInterpretOldSnapshotCursor(t *testing.T) {
st := openLocalDiscoveryTestStore(t)
if _, err := st.db.Exec(`INSERT INTO local_v02_task_discovery_state(dispatcher_id,cursor,last_mode,ready,updated_at) VALUES('d-1','opaque-old','snapshot',1,'now')`); err != nil {
t.Fatal(err)
}
if err := st.ApplyLocalTaskDiscoveryPage(eventPage("0", "1", eventTask("task-a", "running", 1))); !errors.Is(err, ErrLocalDiscoveryUnavailable) {
t.Fatalf("legacy cursor should block, got %v", err)
}
var count int
if err := st.db.QueryRow(`SELECT COUNT(*) FROM local_v03_task_discovery_state WHERE dispatcher_id='d-1'`).Scan(&count); err != nil || count != 0 {
t.Fatalf("legacy cursor created new checkpoint: count=%d err=%v", count, err)
}
}
-78
View File
@@ -1,78 +0,0 @@
package store
import (
"errors"
"strings"
"testing"
)
func TestEventPageWriteFailureRollsBackAssignmentCursorAndEvidence(t *testing.T) {
st := openLocalDiscoveryTestStore(t)
if err := st.ApplyLocalTaskDiscoveryPage(eventPage("0", "1", eventTask("task-a", "running", 1))); err != nil {
t.Fatal(err)
}
var before []byte
if err := st.db.QueryRow(`SELECT body FROM local_v03_task_discovery_state WHERE dispatcher_id='d-1'`).Scan(&before); err != nil {
t.Fatal(err)
}
if _, err := st.db.Exec(`CREATE TRIGGER reject_discovery_update BEFORE UPDATE ON local_v01_task_assignments
BEGIN SELECT RAISE(ABORT, 'forced assignment write failure'); END`); err != nil {
t.Fatal(err)
}
err := st.ApplyLocalTaskDiscoveryPage(eventPage("1", "2", eventTask("task-a", "paused", 2)))
if err == nil || !strings.Contains(err.Error(), "forced assignment write failure") {
t.Fatalf("assignment write failure was swallowed: %v", err)
}
cursor, exists, err := st.LocalTaskDiscoveryCursor("d-1")
if err != nil || !exists || cursor != "1" {
t.Fatalf("failed page advanced cursor: %q exists=%v err=%v", cursor, exists, err)
}
assignment, err := st.LocalTaskAssignment("d-1", "task-a")
if err != nil || assignment.Status != "running" || assignment.TaskRevision != 1 {
t.Fatalf("failed page changed assignment: %+v err=%v", assignment, err)
}
var after []byte
if err := st.db.QueryRow(`SELECT body FROM local_v03_task_discovery_state WHERE dispatcher_id='d-1'`).Scan(&after); err != nil || string(after) != string(before) {
t.Fatalf("failed page replaced saved evidence: before=%s after=%s err=%v", before, after, err)
}
}
func TestEventPageCannotUndoMQPauseStopOrRemoval(t *testing.T) {
st := openLocalDiscoveryTestStore(t)
if err := st.ApplyLocalTaskDiscoveryPage(eventPage("0", "1", eventTask("task-a", "running", 1))); err != nil {
t.Fatal(err)
}
if _, err := st.SetLocalTaskAdmissionBarrier("d-1", "task-a", "tenant-a", "tenant-key-a", "paused"); err != nil {
t.Fatal(err)
}
if err := st.ApplyLocalTaskDiscoveryPage(eventPage("1", "2", eventTask("task-a", "running", 2))); err != nil {
t.Fatal(err)
}
assignment, err := st.LocalTaskAssignment("d-1", "task-a")
if err != nil || assignment.AdmissionState != "paused" {
t.Fatalf("stale running discovery overrode MQ pause: %+v err=%v", assignment, err)
}
if _, err := st.SetLocalTaskAdmissionBarrier("d-1", "task-a", "tenant-a", "tenant-key-a", "stopped"); err != nil {
t.Fatal(err)
}
if err := st.ApplyLocalTaskDiscoveryPage(eventPage("2", "3", eventTask("task-a", "running", 3))); err != nil {
t.Fatal(err)
}
assignment, err = st.LocalTaskAssignment("d-1", "task-a")
if err != nil || assignment.AdmissionState != "stopped" {
t.Fatalf("stale running discovery overrode MQ stop: %+v err=%v", assignment, err)
}
if err := st.ApplyLocalTaskDiscoveryPage(eventPage("3", "4", eventTask("task-a", "removed", 3))); err != nil {
t.Fatal(err)
}
if err := st.ApplyLocalTaskDiscoveryPage(eventPage("4", "5", eventTask("task-a", "running", 4))); !errors.Is(err, ErrLocalTaskRemoved) {
t.Fatalf("removed task was revived: %v", err)
}
if err := st.ApplyLocalTaskDiscoveryPage(eventPage("4", "4")); err != nil {
t.Fatal(err)
}
assignments, err := st.LocalTaskAssignments("d-1")
if err != nil || len(assignments) != 1 || !assignments[0].Removed || assignments[0].AdmissionState != "removed" {
t.Fatalf("later event revived removed task: %+v err=%v", assignments, err)
}
}
@@ -6,6 +6,10 @@ import (
"time"
)
func eventTask(id, status string, revision int64) LocalDiscoveredTask {
return LocalDiscoveredTask{TaskID: id, TenantID: "tenant-a", TenantKey: "tenant-key-a", Status: status, TaskRevision: revision}
}
func TestV04CompleteSnapshotReplacesMembershipWithoutReopeningPause(t *testing.T) {
st := openLocalDiscoveryTestStore(t)
now := time.Now().UTC()
+49
View File
@@ -0,0 +1,49 @@
package store
import (
"database/sql"
"errors"
"fmt"
"time"
)
// LocalDiscoveredTaskForConfig binds a config read to a fully caught-up,
// durable task assignment. A partial page cannot authorize configuration.
func (s *Store) LocalDiscoveredTaskForConfig(dispatcherID, taskID, tenantID string) (LocalTaskAssignment, error) {
s.mu.Lock()
defer s.mu.Unlock()
var ready int
var observedAt string
err := s.db.QueryRow(`SELECT ready,updated_at FROM local_v04_task_discovery_state WHERE dispatcher_id=?`, dispatcherID).Scan(&ready, &observedAt)
if errors.Is(err, sql.ErrNoRows) || (err == nil && ready != 1) {
return LocalTaskAssignment{}, ErrLocalDiscoveryUnavailable
}
if err != nil {
return LocalTaskAssignment{}, err
}
var assignment LocalTaskAssignment
var removed int
err = s.db.QueryRow(`SELECT dispatcher_id,task_id,tenant_id,tenant_key,task_revision,saas_status,admission_state,removed,
queue_exchange,routing_key,binding_key,queue_name,updated_at FROM local_v01_task_assignments WHERE dispatcher_id=? AND task_id=?`,
dispatcherID, taskID).Scan(&assignment.DispatcherID, &assignment.TaskID, &assignment.TenantID, &assignment.TenantKey,
&assignment.TaskRevision, &assignment.Status, &assignment.AdmissionState, &removed, &assignment.Queue.Exchange,
&assignment.Queue.RoutingKey, &assignment.Queue.BindingKey, &assignment.Queue.QueueName, &assignment.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
return LocalTaskAssignment{}, ErrLocalDiscoveryTaskMissing
}
if err != nil {
return LocalTaskAssignment{}, err
}
if assignment.TenantID != tenantID {
return LocalTaskAssignment{}, ErrTenantBindingConflict
}
assignment.Removed = removed != 0
if assignment.Removed {
return LocalTaskAssignment{}, ErrLocalTaskRemoved
}
assignment.DiscoveryObservedAt, err = time.Parse(time.RFC3339Nano, observedAt)
if err != nil {
return LocalTaskAssignment{}, fmt.Errorf("invalid durable discovery observation time: %w", err)
}
return assignment, nil
}