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

99 lines
3.6 KiB
Go

package dispatcher
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"git.ipao.vip/rogee/go-sip/internal/configread"
"git.ipao.vip/rogee/go-sip/internal/store"
)
// CurrentBootstrap is the one admission boundary for the current read-only
// configuration contract. Control consumption remains available even when
// opening task admission fails.
type CurrentBootstrap struct {
Client *configread.Client
Store *store.CurrentStore
DispatcherID string
VerifySIP func(context.Context, configread.CurrentSIP) error
DrainControls func(context.Context) error
Cursor *string // memory-only; restart always starts from a full snapshot
SIP *configread.CurrentSIP // the exact approved snapshot verified on startup
}
func (b CurrentBootstrap) Run(ctx context.Context) error {
if b.Client == nil || b.Store == nil || b.DispatcherID == "" || b.VerifySIP == nil || b.DrainControls == nil {
return errors.New("current Dispatcher bootstrap requires config client, durable store, identity, applied SIP verifier, and control drain")
}
if b.Cursor != nil {
*b.Cursor = ""
}
if b.SIP != nil {
*b.SIP = configread.CurrentSIP{}
}
if err := b.Store.CloseAdmission(b.DispatcherID); err != nil {
return fmt.Errorf("close task admission before bootstrap: %w", err)
}
sip, err := b.Client.ReadCurrentSIP(ctx)
if err != nil {
return fmt.Errorf("read approved SIP snapshot: %w", err)
}
if sip.DispatcherID != b.DispatcherID {
return errors.New("SIP snapshot belongs to another Dispatcher")
}
if err := b.VerifySIP(ctx, sip); err != nil {
return fmt.Errorf("verify SIP revision %d loaded by Agent/Asterisk: %w", sip.Revision, err)
}
tasks, cursor, err := b.Client.ReadAllCurrentTasks(ctx)
if err != nil {
return fmt.Errorf("retrieve complete assigned task list: %w", err)
}
if err := b.Store.ApplyDiscoverySnapshot(b.DispatcherID, tasks); err != nil {
return fmt.Errorf("persist complete task discovery: %w", err)
}
for _, task := range tasks {
if task.Status == "stopped" {
continue
}
snapshot, err := b.Client.ReadCurrentTask(ctx, task.TaskID, task.TenantID)
if err != nil {
return fmt.Errorf("read assigned task %q: %w", task.TaskID, err)
}
var approvedTrunks, taskTrunks any
if err := json.Unmarshal(sip.Trunks, &approvedTrunks); err != nil {
return fmt.Errorf("decode approved SIP trunks: %w", err)
}
if err := json.Unmarshal(snapshot.SIP.Trunks, &taskTrunks); err != nil {
return fmt.Errorf("decode task SIP trunks: %w", err)
}
if snapshot.SIP.Revision != sip.Revision || !reflect.DeepEqual(taskTrunks, approvedTrunks) || snapshot.Task.TaskRevision != task.TaskRevision || snapshot.Task.Status != task.Status {
return fmt.Errorf("task %q configuration differs from approved SIP/discovery snapshot", task.TaskID)
}
if err := validateCurrentAISnapshot(snapshot); err != nil {
return err
}
if err := b.Store.SaveSnapshot(snapshot); err != nil {
return fmt.Errorf("persist immutable task %q: %w", task.TaskID, err)
}
}
if err := b.DrainControls(ctx); err != nil {
return fmt.Errorf("drain assigned Dispatcher control queue: %w", err)
}
// Even if a newer SIP notification is still pending, keep the verified
// snapshot and in-memory cursor so control/results can continue while the
// runtime waits with admission closed. No old SIP version is admitted.
if b.Cursor != nil {
*b.Cursor = cursor
}
if b.SIP != nil {
*b.SIP = sip
}
if err := b.Store.MarkReadyForSIP(b.DispatcherID, sip.Revision); err != nil {
return fmt.Errorf("open admitted tasks: %w", err)
}
return nil
}