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

116 lines
4.0 KiB
Go

package dispatcher
import (
"context"
"errors"
"fmt"
"git.ipao.vip/rogee/go-sip/internal/configread"
"git.ipao.vip/rogee/go-sip/internal/store"
)
// Bootstrap is the one admission boundary for the current read-only
// configuration contract. Control consumption remains available even when
// opening task admission fails.
type Bootstrap struct {
Client *configread.Client
Store *store.Store
DispatcherID string
ApplySIP func(context.Context, configread.SIP) error // nil for isolated Mock; real SIP-only must apply before verifying
VerifySIP func(context.Context, configread.SIP) error
DrainControls func(context.Context) error
Cursor *string // memory-only; restart always starts from a full snapshot
SIP *configread.SIP // the exact approved snapshot verified on startup
Providers *map[string]configread.Provider // boot-approved catalog shared by all tasks
}
func (b Bootstrap) 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.SIP{}
}
if b.Providers != nil {
*b.Providers = nil
}
if err := b.Store.CloseAdmission(b.DispatcherID); err != nil {
return fmt.Errorf("close task admission before bootstrap: %w", err)
}
sip, err := b.Client.ReadSIP(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 b.ApplySIP != nil {
if err := b.ApplySIP(ctx, sip); err != nil {
return fmt.Errorf("apply approved SIP revision %d on Agent/Asterisk: %w", sip.Revision, err)
}
}
if err := b.VerifySIP(ctx, sip); err != nil {
return fmt.Errorf("verify SIP revision %d loaded by Agent/Asterisk: %w", sip.Revision, err)
}
providers, err := b.Client.ReadProviders(ctx)
if err != nil {
return fmt.Errorf("read boot-approved AI providers: %w", err)
}
tasks, cursor, err := b.Client.ReadAllTasks(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.ReadTask(ctx, task.TaskID, task.TenantID, sip, providers)
if err != nil {
return fmt.Errorf("read assigned task %q: %w", task.TaskID, err)
}
if snapshot.Task.TaskRevision != task.TaskRevision || snapshot.Task.Status != task.Status {
return fmt.Errorf("task %q configuration differs from approved discovery snapshot", task.TaskID)
}
if err := validateAISnapshot(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 b.SIP != nil {
*b.SIP = sip
}
if b.Providers != nil {
*b.Providers = providers
}
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
}
// A sip.config delivered during control drain remains fenced and is
// reconciled by the runtime notification loop rather than aborting boot.
_, pending, err := b.Store.SIPState(b.DispatcherID)
if err != nil {
return err
}
if pending != 0 {
return nil
}
if err := b.Store.MarkReadyForSIP(b.DispatcherID, sip.Revision); err != nil {
return fmt.Errorf("open admitted tasks: %w", err)
}
return nil
}