Files
go-sip/internal/dispatcher/current_discovery.go
T

83 lines
2.8 KiB
Go

package dispatcher
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"git.ipao.vip/rogee/go-sip/internal/configread"
"git.ipao.vip/rogee/go-sip/internal/store"
)
// CurrentDiscoveryFollower keeps only an in-memory cursor. A restart always
// takes a complete snapshot and drains the control queue before admitting.
type CurrentDiscoveryFollower struct {
DispatcherID string
Client *configread.Client
Store *store.CurrentStore
ApprovedSIP configread.CurrentSIP
VerifySIP func(context.Context, configread.CurrentSIP) error
Cursor string
}
func (f *CurrentDiscoveryFollower) Poll(ctx context.Context) error {
if f == nil || f.Client == nil || f.Store == nil || f.DispatcherID == "" || f.ApprovedSIP.DispatcherID != f.DispatcherID || f.ApprovedSIP.Revision <= 0 || f.VerifySIP == nil || f.Cursor == "" {
return errors.New("discovery follower requires a verified full snapshot and in-memory cursor")
}
approved, err := json.Marshal(f.ApprovedSIP)
if err != nil {
return f.fail(fmt.Errorf("encode approved SIP snapshot: %w", err))
}
for {
page, err := f.Client.ReadCurrentTasks(ctx, f.Cursor)
if err != nil {
return f.fail(fmt.Errorf("read task discovery after cursor: %w", err))
}
if len(page.Tasks) == 0 {
f.Cursor = page.Cursor
return nil
}
if err := f.Store.ApplyDiscoveryPage(f.DispatcherID, page.Tasks); err != nil {
return f.fail(fmt.Errorf("persist discovery page before cursor advance: %w", err))
}
for _, task := range page.Tasks {
if task.Status == "stopped" {
continue
}
snapshot, err := f.Client.ReadCurrentTask(ctx, task.TaskID, task.TenantID)
if err != nil {
return f.fail(fmt.Errorf("load discovered task %q: %w", task.TaskID, err))
}
if snapshot.Task.TaskRevision != task.TaskRevision || snapshot.Task.Status != task.Status {
return f.fail(fmt.Errorf("discovery and task configuration conflict for %q", task.TaskID))
}
actual, err := json.Marshal(snapshot.SIP)
if err != nil {
return f.fail(fmt.Errorf("encode task %q SIP snapshot: %w", task.TaskID, err))
}
if !bytes.Equal(approved, actual) {
return f.fail(fmt.Errorf("task %q SIP differs from the approved loaded revision", task.TaskID))
}
if err := f.VerifySIP(ctx, snapshot.SIP); err != nil {
return f.fail(fmt.Errorf("task %q SIP is not loaded by Agent/Asterisk: %w", task.TaskID, err))
}
if err := validateCurrentAISnapshot(snapshot); err != nil {
return f.fail(err)
}
if err := f.Store.SaveSnapshot(snapshot); err != nil {
return f.fail(fmt.Errorf("bind discovered task %q: %w", task.TaskID, err))
}
}
f.Cursor = page.Cursor
}
}
func (f *CurrentDiscoveryFollower) fail(cause error) error {
if err := f.Store.CloseAdmission(f.DispatcherID); err != nil {
return errors.Join(cause, fmt.Errorf("close admission after discovery failure: %w", err))
}
return cause
}