fix(dispatcher): apply approved SIP before real startup verification
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user