Files
nezha_domains/integration/agentcompat/internal/scenario/held_session_set_topology.go
T
2026-07-20 04:51:20 +00:00

234 lines
9.1 KiB
Go

//go:build linux
package scenario
import (
"context"
"errors"
"fmt"
"reflect"
"github.com/nezhahq/nezha/integration/agentcompat/internal/agent"
"github.com/nezhahq/nezha/integration/agentcompat/internal/client"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
"github.com/nezhahq/nezha/integration/agentcompat/internal/dashboard"
)
var (
ErrInvalidHeldSessionSetTopology = errors.New("held session set topology is invalid")
ErrInvalidHeldSessionSetPlan = errors.New("held session set plan is invalid")
)
const heldSessionSetAgentCount = 8
type HeldSessionAgent struct {
Ordinal StressAgentOrdinal
Agent *agent.Agent
Readiness agent.Readiness
PATClient *client.Client
}
type HeldSessionSetInput struct {
Dashboard *dashboard.Dashboard
Plan StressPlan
Topology []HeldSessionAgent
Dependencies HeldSessionSetDependencies
ControlClient *client.Client
ControlServerIDs []uint64
testHealthSnapshotRequestHook func(int)
testHealthSnapshotReplyHook func(int)
testHealthSnapshotOverrideHook func(int, heldHealthSnapshotRequest) *heldHealthSnapshot
testHealthSnapshotSendHook func(int)
testHealthEventHook func(heldHealthMessage)
testHealthClosureObservedHook func(int)
testHealthSnapshotAcceptedHook func(heldHealthSnapshot)
testHealthShutdownAcceptedHook func()
testHealthShutdownAcknowledgedHook func()
}
type heldSessionSetTopology struct {
dashboard *dashboard.Dashboard
stateClient heldSessionSetStateObserver
agents map[int]HeldSessionAgent
}
type heldSessionSetStateObserver interface {
IOStreamState(context.Context) (client.IOStreamState, error)
WaitForIOStreamState(context.Context, client.IOStreamStateExpectation) (client.IOStreamState, error)
}
type heldSessionSetAuthorizedObserver struct {
client *client.Client
}
func (observer heldSessionSetAuthorizedObserver) IOStreamState(ctx context.Context) (client.IOStreamState, error) {
return observer.client.IOStreamState(ctx)
}
func (observer heldSessionSetAuthorizedObserver) WaitForIOStreamState(ctx context.Context, expectation client.IOStreamStateExpectation) (client.IOStreamState, error) {
return observer.client.WaitForIOStreamState(ctx, expectation)
}
type HeldSessionSetDependencies struct {
Terminal func(context.Context, heldTerminalInput) (heldSession, error)
NAT func(context.Context, heldNATInput) (heldSession, error)
FM func(context.Context, heldLegacyFMInput) (heldSession, error)
Snapshot func(context.Context, heldSessionSetStateObserver) (client.IOStreamState, error)
WaitState func(context.Context, heldSessionSetStateObserver, client.IOStreamStateExpectation) (client.IOStreamState, error)
InspectAgent func(*agent.Agent) heldSessionAgentFacts
ObserveState func(*client.Client) heldSessionSetStateObserver
}
type heldSessionAgentFacts struct {
PID int
UUID string
}
func defaultHeldSessionSetDependencies() HeldSessionSetDependencies {
return HeldSessionSetDependencies{
Terminal: func(ctx context.Context, input heldTerminalInput) (heldSession, error) {
return newHeldTerminalSession(ctx, input)
},
NAT: func(ctx context.Context, input heldNATInput) (heldSession, error) {
return newHeldNATSession(ctx, input)
},
FM: func(ctx context.Context, input heldLegacyFMInput) (heldSession, error) {
return newHeldLegacyFMSession(ctx, input)
},
Snapshot: func(ctx context.Context, stateClient heldSessionSetStateObserver) (client.IOStreamState, error) {
return stateClient.IOStreamState(ctx)
},
WaitState: func(ctx context.Context, stateClient heldSessionSetStateObserver, expectation client.IOStreamStateExpectation) (client.IOStreamState, error) {
return stateClient.WaitForIOStreamState(ctx, expectation)
},
InspectAgent: func(instance *agent.Agent) heldSessionAgentFacts {
return heldSessionAgentFacts{PID: instance.PID(), UUID: instance.UUID()}
},
ObserveState: func(controlClient *client.Client) heldSessionSetStateObserver {
return heldSessionSetAuthorizedObserver{client: controlClient}
},
}
}
func validateHeldSessionSetPlans(plan StressPlan) ([]StressSessionPlan, error) {
canonical, err := canonicalHeldSessionPlans(plan)
if err != nil {
return nil, errors.Join(ErrInvalidHeldSessionSetPlan, err)
}
if !reflect.DeepEqual(canonical, plan.Sessions) || len(plan.Sessions) != 12 {
return nil, ErrInvalidHeldSessionSetPlan
}
ids := make(map[string]struct{}, len(plan.Sessions))
for _, session := range plan.Sessions {
if session.ID.String() == "" {
return nil, fmt.Errorf("empty session plan ID: %w", ErrInvalidHeldSessionSetPlan)
}
if _, exists := ids[session.ID.String()]; exists {
return nil, fmt.Errorf("duplicate session plan ID: %s: %w", session.ID.String(), ErrInvalidHeldSessionSetPlan)
}
ids[session.ID.String()] = struct{}{}
}
if countHeldSessionKind(plan.Sessions, StressSessionTerminal) != 4 || countHeldSessionKind(plan.Sessions, StressSessionNAT) != 4 || countHeldSessionKind(plan.Sessions, StressSessionFM) != 4 {
return nil, ErrInvalidHeldSessionSetPlan
}
return append([]StressSessionPlan(nil), plan.Sessions...), nil
}
func canonicalHeldSessionPlans(plan StressPlan) ([]StressSessionPlan, error) {
if plan.Seed == 0 || plan.Profile == "" {
return nil, ErrInvalidHeldSessionSetPlan
}
profile, err := contract.ProfileByName(string(plan.Profile))
if err != nil {
return nil, err
}
canonical, err := GenerateStressPlan(profile, plan.Seed)
if err != nil {
return nil, err
}
return canonical.Sessions, nil
}
func countHeldSessionKind(plans []StressSessionPlan, kind StressSessionKind) int {
count := 0
for _, plan := range plans {
if plan.Kind == kind {
count++
}
}
return count
}
func validateHeldSessionSetTopology(input HeldSessionSetInput, plans []StressSessionPlan) (heldSessionSetTopology, error) {
if input.Dashboard == nil || input.ControlClient == nil {
return heldSessionSetTopology{}, ErrInvalidHeldSessionSetTopology
}
inspectAgent := input.Dependencies.InspectAgent
if inspectAgent == nil {
inspectAgent = defaultHeldSessionSetDependencies().InspectAgent
}
observeState := input.Dependencies.ObserveState
if observeState == nil {
observeState = defaultHeldSessionSetDependencies().ObserveState
}
profile, err := contract.ProfileByName(string(input.Plan.Profile))
if err != nil || profile.AgentCount() != heldSessionSetAgentCount || len(input.Topology) != heldSessionSetAgentCount {
return heldSessionSetTopology{}, ErrInvalidHeldSessionSetTopology
}
agents := make(map[int]HeldSessionAgent, len(input.Topology))
uuids := make(map[string]struct{}, len(input.Topology))
serverIDs := make(map[uint64]struct{}, len(input.Topology))
for _, topology := range input.Topology {
ordinal := topology.Ordinal.Int()
if ordinal < 1 || ordinal > heldSessionSetAgentCount || topology.Agent == nil || topology.PATClient == nil {
return heldSessionSetTopology{}, ErrInvalidHeldSessionSetTopology
}
if _, exists := agents[ordinal]; exists {
return heldSessionSetTopology{}, fmt.Errorf("duplicate agent ordinal %d: %w", ordinal, ErrInvalidHeldSessionSetTopology)
}
agentFacts := inspectAgent(topology.Agent)
if err := validateHeldReadinessFacts(agentFacts, topology.Readiness); err != nil {
return heldSessionSetTopology{}, err
}
if agentFacts.PID < 1 || topology.Readiness.ServerID == 0 || topology.Readiness.UUID != agentFacts.UUID {
return heldSessionSetTopology{}, ErrInvalidHeldSessionSetTopology
}
if _, exists := uuids[topology.Readiness.UUID]; exists {
return heldSessionSetTopology{}, ErrInvalidHeldSessionSetTopology
}
if _, exists := serverIDs[topology.Readiness.ServerID]; exists {
return heldSessionSetTopology{}, ErrInvalidHeldSessionSetTopology
}
uuids[topology.Readiness.UUID] = struct{}{}
serverIDs[topology.Readiness.ServerID] = struct{}{}
agents[ordinal] = topology
}
if len(input.ControlServerIDs) != heldSessionSetAgentCount {
return heldSessionSetTopology{}, ErrInvalidHeldSessionSetTopology
}
controlServerIDs := make(map[uint64]struct{}, len(input.ControlServerIDs))
for _, serverID := range input.ControlServerIDs {
if serverID == 0 {
return heldSessionSetTopology{}, ErrInvalidHeldSessionSetTopology
}
if _, exists := serverIDs[serverID]; !exists {
return heldSessionSetTopology{}, ErrInvalidHeldSessionSetTopology
}
if _, exists := controlServerIDs[serverID]; exists {
return heldSessionSetTopology{}, ErrInvalidHeldSessionSetTopology
}
controlServerIDs[serverID] = struct{}{}
}
for ordinal := 1; ordinal <= heldSessionSetAgentCount; ordinal++ {
if _, exists := agents[ordinal]; !exists {
return heldSessionSetTopology{}, ErrInvalidHeldSessionSetTopology
}
}
for _, plan := range plans {
if _, exists := agents[plan.Agent.Int()]; !exists {
return heldSessionSetTopology{}, fmt.Errorf("missing agent ordinal %d: %w", plan.Agent.Int(), ErrInvalidHeldSessionSetTopology)
}
}
return heldSessionSetTopology{dashboard: input.Dashboard, stateClient: observeState(input.ControlClient), agents: agents}, nil
}