90 lines
3.4 KiB
Go
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, ¬ice); 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
|
|
}
|