refactor: remove unused v0.3 task discovery runtime
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user