134 lines
5.1 KiB
Go
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
|
|
}
|