116 lines
4.0 KiB
Go
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
|
|
}
|