Files
go-sip/internal/configread/discovery_v04.go
T

134 lines
5.1 KiB
Go

package configread
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"git.ipao.vip/rogee/go-sip/internal/contract"
)
// TaskSnapshotPage belongs to one immutable, complete snapshot of this Dispatcher's tasks.
type TaskSnapshotPage struct {
DispatcherID string
SnapshotID string
Watermark string
NextPageToken string
Tasks []DiscoveredTask
Body json.RawMessage
}
// ReadTaskSnapshot reads either the first snapshot page or the next page of the same snapshot.
// Cross-page identity, completeness, and duplicate checks belong to the caller, before admission.
func (c *Client) ReadTaskSnapshot(ctx context.Context, snapshotID, pageToken string) (TaskSnapshotPage, error) {
var query url.Values
switch {
case snapshotID == "" && pageToken == "":
query = url.Values{"mode": {"snapshot"}}
case snapshotID != "" && pageToken != "":
query = url.Values{"snapshot_id": {snapshotID}, "page_token": {pageToken}}
default:
return TaskSnapshotPage{}, errors.New("snapshot ID and page token must both be present or absent")
}
body, err := c.getTaskDiscoveryV04(ctx, configReadPath+"/tasks?"+query.Encode())
if err != nil {
return TaskSnapshotPage{}, err
}
var response struct {
Mode string `json:"mode"`
DispatcherID string `json:"dispatcher_id"`
SnapshotID string `json:"snapshot_id"`
Watermark string `json:"watermark"`
NextPageToken *string `json:"next_page_token"`
Tasks []DiscoveredTask `json:"tasks"`
}
if err := json.Unmarshal(body, &response); err != nil {
return TaskSnapshotPage{}, fmt.Errorf("decode task snapshot: %w", err)
}
if response.Mode != "snapshot" || response.DispatcherID != c.dispatcherID {
return TaskSnapshotPage{}, errors.New("task snapshot mode or dispatcher identity mismatch")
}
if _, err := parseEventCursor(response.Watermark); err != nil {
return TaskSnapshotPage{}, fmt.Errorf("invalid task snapshot watermark: %w", err)
}
if snapshotID != "" && response.SnapshotID != snapshotID {
return TaskSnapshotPage{}, errors.New("task snapshot identity changed between pages")
}
if err := validateV04Tasks(response.Tasks, false); err != nil {
return TaskSnapshotPage{}, err
}
var next string
if response.NextPageToken != nil {
next = *response.NextPageToken
if len(response.Tasks) == 0 || next == pageToken {
return TaskSnapshotPage{}, errors.New("task snapshot has an empty or repeated next page")
}
}
return TaskSnapshotPage{DispatcherID: response.DispatcherID, SnapshotID: response.SnapshotID,
Watermark: response.Watermark, NextPageToken: next, Tasks: response.Tasks, Body: append(json.RawMessage(nil), body...)}, nil
}
// ReadTaskChanges uses the in-memory cursor acquired from a completed startup snapshot.
func (c *Client) ReadTaskChanges(ctx context.Context, after string) (TaskDiscovery, error) {
from, err := parseEventCursor(after)
if err != nil {
return TaskDiscovery{}, fmt.Errorf("invalid live discovery cursor: %w", err)
}
body, err := c.getTaskDiscoveryV04(ctx, configReadPath+"/tasks?"+url.Values{"after": {after}}.Encode())
if err != nil {
return TaskDiscovery{}, err
}
var response struct {
Mode string `json:"mode"`
DispatcherID string `json:"dispatcher_id"`
NextCursor string `json:"next_cursor"`
Tasks []DiscoveredTask `json:"tasks"`
}
if err := json.Unmarshal(body, &response); err != nil {
return TaskDiscovery{}, fmt.Errorf("decode live task changes: %w", err)
}
if response.Mode != "changes" || response.DispatcherID != c.dispatcherID {
return TaskDiscovery{}, errors.New("live task changes mode or dispatcher identity mismatch")
}
to, err := parseEventCursor(response.NextCursor)
if err != nil || (len(response.Tasks) == 0 && to != from) || (len(response.Tasks) != 0 && to <= from) {
return TaskDiscovery{}, errors.New("live task changes cursor does not match returned tasks")
}
if err := validateV04Tasks(response.Tasks, true); 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 validateV04Tasks(tasks []DiscoveredTask, allowRemoved bool) error {
seen := make(map[string]struct{}, len(tasks))
tenants, keys := make(map[string]string), make(map[string]string)
for _, task := range tasks {
if !allowRemoved && task.Status == "removed" {
return errors.New("complete task snapshot cannot include removed tombstones")
}
if _, duplicate := seen[task.TaskID]; duplicate {
return fmt.Errorf("task discovery contains duplicate task ID %q", task.TaskID)
}
seen[task.TaskID] = struct{}{}
if err := validateTenantBinding(tenants, keys, task.TenantID, task.TenantKey); err != nil {
return err
}
}
return nil
}
func (c *Client) getTaskDiscoveryV04(ctx context.Context, path string) (json.RawMessage, error) {
body, err := c.get(ctx, path)
if err != nil {
return nil, err
}
if err := contract.ValidateLocalTaskDiscoveryV04(body); err != nil {
return nil, fmt.Errorf("validate v0.4 task discovery response: %w", err)
}
return body, nil
}