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

90 lines
3.4 KiB
Go

package dispatcher
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"git.ipao.vip/rogee/go-sip/internal/configread"
"git.ipao.vip/rogee/go-sip/internal/contract"
"git.ipao.vip/rogee/go-sip/internal/store"
)
// SIPOnly is a distinct update lane: it never discovers tasks or enables
// call admission, even when native Asterisk confirms the entire snapshot.
type SIPOnly struct {
DispatcherID string
Store *store.Store
Client *configread.Client
ApplySIP func(context.Context, configread.SIP) error
VerifySIP func(context.Context, configread.SIP) error
}
// HandleNotification persists the assigned SIP revision before the broker ACK.
// Non-SIP controls must be requeued for a business Dispatcher, never swallowed.
func (s SIPOnly) HandleNotification(_ context.Context, route string, body []byte) error {
if s.Store == nil || s.DispatcherID == "" || route != "d."+s.DispatcherID+".control.in" {
return errors.New("SIP-only notification has no approved queue owner")
}
if err := contract.ValidateCurrent("mq", body); err != nil {
return fmt.Errorf("SIP-only notification violates current contract: %w", err)
}
var notice struct {
EventType string `json:"event_type"`
DispatcherID string `json:"dispatcher_id"`
Payload struct {
Revision int64 `json:"revision"`
} `json:"payload"`
}
if err := json.Unmarshal(body, &notice); err != nil {
return err
}
if notice.DispatcherID != s.DispatcherID || notice.EventType != "sip.config" || notice.Payload.Revision <= 0 {
return errors.New("SIP-only cannot consume another Dispatcher or business control")
}
if err := s.Store.NoteSIPChange(s.DispatcherID, notice.Payload.Revision); err != nil {
return err
}
slog.Info("SIP-only notification durably fenced", "dispatcher_id", s.DispatcherID, "revision", notice.Payload.Revision)
return nil
}
// Sync fetches only the complete SIP snapshot; it never reads task, quota or
// AI config. The Agent revision and actual native load must both agree before
// the durable pending revision is cleared, while admission remains closed.
func (s SIPOnly) Sync(ctx context.Context) error {
if s.Store == nil || s.Client == nil || s.DispatcherID == "" || s.ApplySIP == nil || s.VerifySIP == nil {
return errors.New("SIP-only sync requires a bound client, durable store and native Agent")
}
sip, err := s.Client.ReadSIP(ctx)
if err != nil {
return fmt.Errorf("read approved SIP-only snapshot: %w", err)
}
if sip.DispatcherID != s.DispatcherID || sip.Revision <= 0 {
return errors.New("SIP-only snapshot owner or revision is invalid")
}
applied, pending, err := s.Store.SIPState(s.DispatcherID)
if err != nil {
return err
}
if sip.Revision < pending || sip.Revision < applied {
return fmt.Errorf("approved SIP snapshot revision %d is behind durable applied=%d pending=%d", sip.Revision, applied, pending)
}
if err := s.Store.NoteSIPChange(s.DispatcherID, sip.Revision); err != nil {
return err
}
if err := s.ApplySIP(ctx, sip); err != nil {
return fmt.Errorf("native SIP apply revision %d: %w", sip.Revision, err)
}
if err := s.VerifySIP(ctx, sip); err != nil {
return fmt.Errorf("native SIP load revision %d: %w", sip.Revision, err)
}
if err := s.Store.MarkSIPOnlyVerified(s.DispatcherID, sip.Revision); err != nil {
return err
}
slog.Info("SIP-only native revision verified without call admission", "dispatcher_id", s.DispatcherID, "revision", sip.Revision)
return nil
}