99 lines
3.6 KiB
Go
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
|
|
}
|