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
-61
View File
@@ -56,7 +56,6 @@ type Snapshot struct {
QuotaRevision int64
TenantMaxConcurrentCalls int64
QuotaValidUntil time.Time
DiscoveryCursor string
FetchedAt time.Time
ExpiresAt time.Time
SIP json.RawMessage
@@ -221,49 +220,6 @@ func (c *Client) ReadTaskStatus(ctx context.Context, taskID, tenantID, tenantKey
}, nil
}
// ReadTaskDiscovery reads one page of latest task states after a durable event ID.
// SaaS guarantees page completeness and ordering; no per-item event ID is provided.
func (c *Client) ReadTaskDiscovery(ctx context.Context, after string) (TaskDiscovery, error) {
from, err := parseEventCursor(after)
if err != nil {
return TaskDiscovery{}, fmt.Errorf("invalid task-discovery after cursor: %w", err)
}
path := configReadPath + "/tasks?" + url.Values{"after": {after}}.Encode()
body, err := c.getTaskDiscovery(ctx, path)
if err != nil {
return TaskDiscovery{}, err
}
var response taskDiscoveryResponse
if err := json.Unmarshal(body, &response); err != nil {
return TaskDiscovery{}, fmt.Errorf("decode task discovery: %w", err)
}
if response.DispatcherID != c.dispatcherID {
return TaskDiscovery{}, errors.New("task-discovery dispatcher identity does not match the request")
}
to, err := parseEventCursor(response.NextCursor)
if err != nil {
return TaskDiscovery{}, fmt.Errorf("invalid task-discovery next cursor: %w", err)
}
if (len(response.Tasks) == 0 && to != from) || (len(response.Tasks) != 0 && to <= from) {
return TaskDiscovery{}, errors.New("task-discovery page cursor does not match returned tasks")
}
seen := make(map[string]struct{}, len(response.Tasks))
tenantByID, idByTenant := make(map[string]string), make(map[string]string)
for _, task := range response.Tasks {
if _, duplicate := seen[task.TaskID]; duplicate {
return TaskDiscovery{}, fmt.Errorf("task discovery contains duplicate task ID %q", task.TaskID)
}
seen[task.TaskID] = struct{}{}
if err := validateTenantBinding(tenantByID, idByTenant, task.TenantID, task.TenantKey); err != nil {
return TaskDiscovery{}, err
}
}
return TaskDiscovery{
DispatcherID: response.DispatcherID, FromCursor: after, NextCursor: response.NextCursor,
Tasks: response.Tasks, Body: append(json.RawMessage(nil), body...),
}, nil
}
func parseEventCursor(cursor string) (uint64, error) {
value, err := strconv.ParseUint(cursor, 10, 64)
if err != nil || strconv.FormatUint(value, 10) != cursor {
@@ -298,17 +254,6 @@ func (c *Client) getConfig(ctx context.Context, path string) (json.RawMessage, e
return body, nil
}
func (c *Client) getTaskDiscovery(ctx context.Context, path string) (json.RawMessage, error) {
body, err := c.get(ctx, path)
if err != nil {
return nil, err
}
if err := contract.ValidateLocalTaskDiscovery(body); err != nil {
return nil, fmt.Errorf("validate task-discovery response: %w", err)
}
return body, nil
}
func (c *Client) get(ctx context.Context, path string) (json.RawMessage, error) {
relative, err := url.Parse(path)
if err != nil {
@@ -373,12 +318,6 @@ type sipConfigResponse struct {
} `json:"artifact"`
}
type taskDiscoveryResponse struct {
DispatcherID string `json:"dispatcher_id"`
NextCursor string `json:"next_cursor"`
Tasks []DiscoveredTask `json:"tasks"`
}
type taskConfigResponse struct {
Resource string `json:"resource"`
DispatcherID string `json:"dispatcher_id"`
-134
View File
@@ -1,134 +0,0 @@
package configread
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
)
const eventTestDispatcherID = "11111111-1111-4111-8111-111111111111"
func eventTestClient(t *testing.T, server *httptest.Server) *Client {
t.Helper()
client, err := NewClient(server.URL, eventTestDispatcherID, mockTestSecret, server.Client())
if err != nil {
t.Fatal(err)
}
return client
}
func TestEventCursorPagesUseOneTaskListAndAllowRepeatedTaskIDs(t *testing.T) {
pages := map[string]string{
"0": "task-discovery-page-v0.3.json",
"2": "task-discovery-updated-v0.3.json",
"3": "task-discovery-removed-v0.3.json",
"4": "task-discovery-empty-v0.3.json",
}
var got []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got = append(got, r.URL.RequestURI())
if r.Header.Get(dispatcherIDHeader) != eventTestDispatcherID || r.Header.Get(dispatcherSecretHeader) != mockTestSecret {
t.Error("Dispatcher credentials were not sent")
}
name := pages[r.URL.Query().Get("after")]
if name == "" || r.URL.Query().Has("page_token") {
t.Errorf("unexpected task request: %s", r.URL.RequestURI())
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(readConfigFixture(t, name))
}))
defer server.Close()
client := eventTestClient(t, server)
cursor := "0"
var statuses []string
for range 4 {
page, err := client.ReadTaskDiscovery(context.Background(), cursor)
if err != nil {
t.Fatal(err)
}
for _, task := range page.Tasks {
statuses = append(statuses, task.TaskID+":"+task.Status)
}
cursor = page.NextCursor
}
if cursor != "4" || !reflect.DeepEqual(statuses, []string{"a01:running", "a02:paused", "a01:paused", "a02:removed"}) {
t.Fatalf("statuses=%v cursor=%q", statuses, cursor)
}
want := []string{
"/internal/v1/dispatcher/tasks?after=0",
"/internal/v1/dispatcher/tasks?after=2",
"/internal/v1/dispatcher/tasks?after=3",
"/internal/v1/dispatcher/tasks?after=4",
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("queries=%v want=%v", got, want)
}
}
func TestEventCursorRejectsBrokenPagesWithoutFallback(t *testing.T) {
valid := string(readConfigFixture(t, "task-discovery-page-v0.3.json"))
var oversized map[string]any
if err := json.Unmarshal([]byte(valid), &oversized); err != nil {
t.Fatal(err)
}
rows := make([]any, 257)
for i := range rows {
rows[i] = oversized["tasks"].([]any)[0]
}
oversized["tasks"] = rows
oversizedBody, err := json.Marshal(oversized)
if err != nil {
t.Fatal(err)
}
cases := map[string]string{
"nonprogress": strings.Replace(valid, `"next_cursor": "2"`, `"next_cursor": "0"`, 1),
"duplicate": strings.Replace(valid, `"task_id": "a02"`, `"task_id": "a01"`, 1),
"wrong dispatcher": strings.Replace(valid, eventTestDispatcherID, "44444444-4444-4444-8444-444444444444", 1),
"oversized page": string(oversizedBody),
"changes": string(readConfigFixture(t, "task-discovery-invalid-changes-v0.3.json")),
"cursor format": string(readConfigFixture(t, "task-discovery-invalid-cursor-v0.3.json")),
"old contract": string(readConfigFixture(t, "task-discovery-snapshot-v0.2.json")),
}
for name, body := range cases {
t.Run(name, func(t *testing.T) {
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(body))
}))
defer server.Close()
client := eventTestClient(t, server)
if _, err := client.ReadTaskDiscovery(context.Background(), "0"); err == nil {
t.Fatal("invalid task page was accepted")
}
if requests != 1 {
t.Fatalf("requests=%d, want one without fallback", requests)
}
})
}
}
func TestEventCursorRejectsExpiredStatusWithoutReset(t *testing.T) {
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusGone)
_, _ = w.Write([]byte(`{"schema_version":"task-discovery.v0.3-proposal","resource":"error","error":{"code":"cursor_expired","message":"expired"}}`))
}))
defer server.Close()
client := eventTestClient(t, server)
if _, err := client.ReadTaskDiscovery(context.Background(), "1"); err == nil {
t.Fatal("HTTP 410 accepted")
}
if requests != 1 {
t.Fatalf("requests=%d, want one without reset to initial cursor", requests)
}
}
+11
View File
@@ -9,6 +9,17 @@ import (
"testing"
)
const eventTestDispatcherID = "11111111-1111-4111-8111-111111111111"
func eventTestClient(t *testing.T, server *httptest.Server) *Client {
t.Helper()
client, err := NewClient(server.URL, eventTestDispatcherID, mockTestSecret, server.Client())
if err != nil {
t.Fatal(err)
}
return client
}
func TestV04SnapshotAndLiveChangesUseSeparateRequests(t *testing.T) {
var paths []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {