83 lines
2.8 KiB
Go
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
|
|
}
|