fix(dispatcher): apply approved SIP before real startup verification

This commit is contained in:
2026-10-09 16:20:37 +08:00
parent f323e31b6c
commit b37d9f3af0
10 changed files with 339 additions and 3 deletions
+5 -1
View File
@@ -135,10 +135,14 @@ func runDispatcher(ctx context.Context, mode string) (result error) {
return coordinator.ApprovedMeta(callCtx, agentEndpoint.AgentID)
},
}
applySIP, err := dispatcherSIPApplier(mode, originator)
if err != nil {
return err
}
worker := &dispatcher.Runtime{
Broker: broker,
Bootstrap: dispatcher.Bootstrap{
DispatcherID: settings.DispatcherID, Client: reader, Store: database, VerifySIP: originator.VerifySIP,
DispatcherID: settings.DispatcherID, Client: reader, Store: database, ApplySIP: applySIP, VerifySIP: originator.VerifySIP, Logger: slog.Default(),
},
Execute: dispatcher.ExecuteController{
DispatcherID: settings.DispatcherID, Store: database, Originator: originator, Publisher: broker, Now: time.Now,
+26
View File
@@ -0,0 +1,26 @@
package main
import (
"context"
"errors"
"fmt"
"git.ipao.vip/rogee/go-sip/internal/configread"
"git.ipao.vip/rogee/go-sip/internal/dispatcher"
)
func dispatcherSIPApplier(mode string, originator *dispatcher.ApprovedOriginator) (func(context.Context, configread.SIP) error, error) {
switch mode {
case "nonprod-real":
if originator == nil || originator.DispatcherID == "" || originator.Client == nil || originator.Meta == nil {
return nil, errors.New("real Dispatcher requires an active Agent SIP loading session")
}
return originator.ApplySIP, nil
case "mock":
// Mock's fixed, explicitly preloaded fixture is only verified. It must
// never issue a native reload or pretend that Asterisk loaded it.
return nil, nil
default:
return nil, fmt.Errorf("unsupported business SIP loading mode %q", mode)
}
}
@@ -0,0 +1,150 @@
package main
import (
"context"
"encoding/json"
"errors"
"os"
"reflect"
"testing"
agentpb "git.ipao.vip/rogee/go-sip/gen/agent"
"git.ipao.vip/rogee/go-sip/internal/configread"
"git.ipao.vip/rogee/go-sip/internal/dispatcher"
"google.golang.org/grpc"
)
type startupSIPAgent struct {
agentpb.AgentControlServiceClient
loaded map[string]int64
actions []string
applyErr error
request *agentpb.ApplySIPRequest
}
func (a *startupSIPAgent) ApplySIP(_ context.Context, request *agentpb.ApplySIPRequest, _ ...grpc.CallOption) (*agentpb.ApplySIPResponse, error) {
a.actions = append(a.actions, "apply")
a.request = request
if a.applyErr != nil {
return nil, a.applyErr
}
var snapshot configread.SIP
if err := json.Unmarshal(request.ApprovedSnapshotJson, &snapshot); err != nil {
return nil, err
}
var trunks []struct {
ID string `json:"trunk_id"`
Enabled bool `json:"enabled"`
}
if err := json.Unmarshal(snapshot.Trunks, &trunks); err != nil {
return nil, err
}
a.loaded = make(map[string]int64)
for _, trunk := range trunks {
if trunk.Enabled {
a.loaded[trunk.ID] = snapshot.Revision
}
}
return &agentpb.ApplySIPResponse{TrunkRevision: a.loaded}, nil
}
func (a *startupSIPAgent) GetLoadedSIP(context.Context, *agentpb.GetLoadedSIPRequest, ...grpc.CallOption) (*agentpb.GetLoadedSIPResponse, error) {
a.actions = append(a.actions, "verify")
return &agentpb.GetLoadedSIPResponse{TrunkRevision: a.loaded}, nil
}
func startupSIPFixture(t *testing.T) (*dispatcher.ApprovedOriginator, *startupSIPAgent, configread.SIP) {
t.Helper()
body, err := os.ReadFile("../../contracts/schema/examples/config-read-sip.json")
if err != nil {
t.Fatal(err)
}
var snapshot configread.SIP
if err := json.Unmarshal(body, &snapshot); err != nil {
t.Fatal(err)
}
snapshot.Revision = 16
var trunks []struct {
ID string `json:"trunk_id"`
Enabled bool `json:"enabled"`
}
if err := json.Unmarshal(snapshot.Trunks, &trunks); err != nil {
t.Fatal(err)
}
agent := &startupSIPAgent{loaded: make(map[string]int64)}
for _, trunk := range trunks {
if trunk.Enabled {
agent.loaded[trunk.ID] = 14
}
}
originator := &dispatcher.ApprovedOriginator{
DispatcherID: snapshot.DispatcherID,
Client: agent,
Meta: func(context.Context) (*agentpb.RequestMeta, error) {
return &agentpb.RequestMeta{AgentId: "agent-test", CellId: "cell-test", BootId: "boot-test", DispatcherEpoch: "epoch-test", SessionGeneration: 1}, nil
},
}
return originator, agent, snapshot
}
func TestDispatcherRealSIPApplierLoadsOldAgentBeforeVerification(t *testing.T) {
originator, agent, snapshot := startupSIPFixture(t)
if err := originator.VerifySIP(context.Background(), snapshot); err == nil {
t.Fatal("test Agent must start on the old revision")
}
agent.actions = nil
apply, err := dispatcherSIPApplier("nonprod-real", originator)
if err != nil {
t.Fatal(err)
}
if apply == nil {
t.Fatal("real Dispatcher startup omitted the mandatory SIP loading step")
}
if err := apply(context.Background(), snapshot); err != nil {
t.Fatal(err)
}
if err := originator.VerifySIP(context.Background(), snapshot); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(agent.actions, []string{"apply", "verify"}) {
t.Fatalf("SIP lifecycle = %v", agent.actions)
}
if agent.request == nil || agent.request.Meta.IdempotencyKey == "" {
t.Fatal("SIP load lost its full snapshot or operation identity")
}
}
func TestDispatcherRealSIPApplierPreservesLoadFailure(t *testing.T) {
originator, agent, snapshot := startupSIPFixture(t)
failure := errors.New("native reload failed")
agent.applyErr = failure
apply, err := dispatcherSIPApplier("nonprod-real", originator)
if err != nil {
t.Fatal(err)
}
if apply == nil {
t.Fatal("real Dispatcher has no SIP loader")
}
if err := apply(context.Background(), snapshot); !errors.Is(err, failure) {
t.Fatalf("SIP load error was hidden: %v", err)
}
if err := originator.VerifySIP(context.Background(), snapshot); err == nil {
t.Fatal("failed native load must not be reported as the new revision")
}
}
func TestDispatcherSIPApplierRequiresRealSessionAndKeepsMockPreloaded(t *testing.T) {
for _, originator := range []*dispatcher.ApprovedOriginator{nil, {DispatcherID: "test"}} {
if _, err := dispatcherSIPApplier("nonprod-real", originator); err == nil {
t.Fatal("real Dispatcher accepted a missing SIP loading session")
}
}
if apply, err := dispatcherSIPApplier("mock", nil); err != nil || apply != nil {
t.Fatal("isolated Mock must only verify its preloaded fixture, not issue native SIP loads")
}
for _, mode := range []string{"", "real", "mixed", "sip-only"} {
if _, err := dispatcherSIPApplier(mode, nil); err == nil {
t.Fatalf("unsupported business mode %q was accepted", mode)
}
}
}