mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
test(agentcompat): model FD diagnostic lifecycles
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
//go:build linux
|
||||
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/agent"
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
|
||||
processharness "github.com/nezhahq/nezha/integration/agentcompat/internal/process"
|
||||
)
|
||||
|
||||
type fdDiagnosticWindowPair struct {
|
||||
baseline fdDiagnosticAgentWindow
|
||||
end fdDiagnosticAgentWindow
|
||||
}
|
||||
|
||||
type stressDiagnosticAgentWindowSpec struct {
|
||||
Ordinal int
|
||||
PID int
|
||||
BaselineCount int
|
||||
EndCount int
|
||||
Target string
|
||||
BaselineSampledAt time.Time
|
||||
EndSampledAt time.Time
|
||||
}
|
||||
|
||||
type stressDiagnosticProcessWindowSpec struct {
|
||||
PID int
|
||||
Count int
|
||||
Target string
|
||||
SampledAt time.Time
|
||||
}
|
||||
|
||||
type stressDiagnosticDashboardWindowSpec struct {
|
||||
PID int
|
||||
BaselineCount int
|
||||
EndCount int
|
||||
}
|
||||
|
||||
func stressDiagnosticAgentWindow(t *testing.T, spec stressDiagnosticAgentWindowSpec) fdDiagnosticWindowPair {
|
||||
t.Helper()
|
||||
if spec.BaselineSampledAt.IsZero() {
|
||||
spec.BaselineSampledAt = time.Unix(int64(spec.Ordinal), 0).UTC()
|
||||
}
|
||||
if spec.EndSampledAt.IsZero() {
|
||||
spec.EndSampledAt = spec.BaselineSampledAt.Add(time.Minute)
|
||||
}
|
||||
agentOrdinal, err := NewStressAgentOrdinal(spec.Ordinal)
|
||||
require.NoError(t, err)
|
||||
process, err := NewStressAgentProcess(agentOrdinal, spec.PID)
|
||||
require.NoError(t, err)
|
||||
identity := agent.ProcessIdentity{Generation: 1, PID: spec.PID}
|
||||
return fdDiagnosticWindowPair{
|
||||
baseline: fdDiagnosticAgentWindow{Process: process, Identity: identity, Window: stressDiagnosticProcessWindow(stressDiagnosticProcessWindowSpec{PID: spec.PID, Count: spec.BaselineCount, Target: spec.Target, SampledAt: spec.BaselineSampledAt})},
|
||||
end: fdDiagnosticAgentWindow{Process: process, Identity: identity, Window: stressDiagnosticProcessWindow(stressDiagnosticProcessWindowSpec{PID: spec.PID, Count: spec.EndCount, Target: spec.Target, SampledAt: spec.EndSampledAt})},
|
||||
}
|
||||
}
|
||||
|
||||
func stressDiagnosticDashboardWindow(t *testing.T, spec stressDiagnosticDashboardWindowSpec) fdDiagnosticWindowPair {
|
||||
t.Helper()
|
||||
process, err := NewStressDashboardProcess(spec.PID)
|
||||
require.NoError(t, err)
|
||||
return fdDiagnosticWindowPair{
|
||||
baseline: fdDiagnosticAgentWindow{Process: process, Window: stressDiagnosticProcessWindow(stressDiagnosticProcessWindowSpec{PID: spec.PID, Count: spec.BaselineCount, Target: "dashboard"})},
|
||||
end: fdDiagnosticAgentWindow{Process: process, Window: stressDiagnosticProcessWindow(stressDiagnosticProcessWindowSpec{PID: spec.PID, Count: spec.EndCount, Target: "dashboard"})},
|
||||
}
|
||||
}
|
||||
|
||||
func stressDiagnosticProcessWindow(spec stressDiagnosticProcessWindowSpec) processharness.Window {
|
||||
samples := make([]processharness.Sample, contract.ResourceSampleCount)
|
||||
for index := range samples {
|
||||
samples[index] = processharness.Sample{PID: spec.PID, NonStdioFDCount: spec.Count, FDObservations: []processharness.FDObservation{{Number: 3, Target: spec.Target}}, SampledAt: spec.SampledAt}
|
||||
}
|
||||
return processharness.Window{PID: spec.PID, Samples: samples}
|
||||
}
|
||||
|
||||
func stressDiagnosticSample(ordinal int, observations []processharness.FDObservation) fdDiagnosticSample {
|
||||
return fdDiagnosticSample{Ordinal: ordinal, FDObservations: observations}
|
||||
}
|
||||
|
||||
func fdDiagnosticTestSampler(_ context.Context, pid int) (processharness.Sample, error) {
|
||||
return processharness.Sample{PID: pid, FDObservations: []processharness.FDObservation{{Number: 4, Target: fmt.Sprintf("added-%d", pid)}}, SampledAt: time.Unix(int64(pid), 0).UTC()}, nil
|
||||
}
|
||||
|
||||
func mustStressPRFullProfile(t *testing.T) contract.Profile {
|
||||
t.Helper()
|
||||
profile, err := contract.ProfileByName(string(contract.ProfilePRFull))
|
||||
require.NoError(t, err)
|
||||
return profile
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
//go:build linux
|
||||
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/agent"
|
||||
processharness "github.com/nezhahq/nezha/integration/agentcompat/internal/process"
|
||||
)
|
||||
|
||||
type fdDiagnosticAgentWindow struct {
|
||||
Process StressProcessIdentity
|
||||
Identity agent.ProcessIdentity
|
||||
Window processharness.Window
|
||||
}
|
||||
|
||||
type fdDiagnosticCandidate struct {
|
||||
Baseline fdDiagnosticAgentWindow
|
||||
End fdDiagnosticAgentWindow
|
||||
}
|
||||
|
||||
type fdDiagnosticSample struct {
|
||||
Ordinal int `json:"sample_ordinal"`
|
||||
PID int `json:"pid"`
|
||||
RSSBytes uint64 `json:"rss_bytes"`
|
||||
DescendantPIDs []int `json:"descendant_pids"`
|
||||
DescendantCount int `json:"descendant_count"`
|
||||
NonStdioFDCount int `json:"non_stdio_fd_count"`
|
||||
TCPListenerCount int `json:"tcp_listener_count"`
|
||||
TCP6ListenerCount int `json:"tcp6_listener_count"`
|
||||
FDObservations []processharness.FDObservation `json:"fd_observations"`
|
||||
ObservedAt time.Time `json:"observed_at"`
|
||||
}
|
||||
|
||||
type fdDiagnosticWindow struct {
|
||||
PID int `json:"pid"`
|
||||
Samples []fdDiagnosticSample `json:"samples"`
|
||||
}
|
||||
|
||||
type fdDiagnosticLifecycle struct {
|
||||
Observation processharness.FDObservation `json:"observation"`
|
||||
Status string `json:"status"`
|
||||
NumberReused bool `json:"number_reused,omitempty"`
|
||||
}
|
||||
|
||||
type fdDiagnosticRecord struct {
|
||||
SamplerPID int `json:"sampler_pid"`
|
||||
AgentOrdinal int `json:"agent_ordinal"`
|
||||
AgentPID int `json:"agent_pid"`
|
||||
BaselineGeneration uint64 `json:"baseline_generation"`
|
||||
BaselinePID int `json:"baseline_pid"`
|
||||
EndPID int `json:"end_pid"`
|
||||
EndGeneration uint64 `json:"end_generation"`
|
||||
Baseline *fdDiagnosticWindow `json:"baseline"`
|
||||
End *fdDiagnosticWindow `json:"end"`
|
||||
Tail []fdDiagnosticSample `json:"tail"`
|
||||
AddedFinal []processharness.FDObservation `json:"added_final"`
|
||||
RemovedFinal []processharness.FDObservation `json:"removed_final"`
|
||||
Lifecycle []fdDiagnosticLifecycle `json:"lifecycle"`
|
||||
LifecycleStatus string `json:"lifecycle_status"`
|
||||
DiagnosticError string `json:"diagnostic_error,omitempty"`
|
||||
}
|
||||
|
||||
func fdDiagnosticEnabled(value string) bool { return value == "1" }
|
||||
|
||||
func newFDDiagnosticRecord(candidate fdDiagnosticCandidate, samplerPID int) (fdDiagnosticRecord, bool) {
|
||||
record := fdDiagnosticRecord{
|
||||
SamplerPID: samplerPID,
|
||||
AgentOrdinal: candidate.Baseline.Process.Agent.Int(),
|
||||
AgentPID: candidate.Baseline.Identity.PID,
|
||||
BaselineGeneration: candidate.Baseline.Identity.Generation,
|
||||
BaselinePID: candidate.Baseline.Identity.PID,
|
||||
EndPID: candidate.End.Identity.PID,
|
||||
EndGeneration: candidate.End.Identity.Generation,
|
||||
Baseline: fdDiagnosticWindowFromProcess(candidate.Baseline.Window),
|
||||
End: fdDiagnosticWindowFromProcess(candidate.End.Window),
|
||||
}
|
||||
if !fdDiagnosticIdentityMatches(candidate) {
|
||||
record.LifecycleStatus = "process_identity_changed"
|
||||
return record, false
|
||||
}
|
||||
baselineFinal := fdDiagnosticFinalObservations(candidate.Baseline.Window)
|
||||
endFinal := fdDiagnosticFinalObservations(candidate.End.Window)
|
||||
record.AddedFinal = fdDiagnosticDifference(endFinal, baselineFinal)
|
||||
record.RemovedFinal = fdDiagnosticDifference(baselineFinal, endFinal)
|
||||
record.LifecycleStatus = "tail_pending"
|
||||
return record, true
|
||||
}
|
||||
|
||||
func classifyFDDiagnosticLifecycle(added []processharness.FDObservation, tail []fdDiagnosticSample) []fdDiagnosticLifecycle {
|
||||
result := make([]fdDiagnosticLifecycle, 0, len(added))
|
||||
for _, observation := range added {
|
||||
present := make([]bool, len(tail))
|
||||
reused := false
|
||||
for index, sample := range tail {
|
||||
for _, tailObservation := range sample.FDObservations {
|
||||
if tailObservation.Number == observation.Number && tailObservation.Target != observation.Target {
|
||||
reused = true
|
||||
}
|
||||
if tailObservation == observation {
|
||||
present[index] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
status := "intermittent_or_reused"
|
||||
switch {
|
||||
case reused:
|
||||
status = "intermittent_or_reused"
|
||||
case !fdDiagnosticAnyPresent(present):
|
||||
status = "cleared_before_tail"
|
||||
case fdDiagnosticAllPresent(present):
|
||||
status = "observed_through_tail"
|
||||
case fdDiagnosticPrefixPresent(present):
|
||||
status = "cleared_during_tail"
|
||||
}
|
||||
result = append(result, fdDiagnosticLifecycle{Observation: observation, Status: status, NumberReused: reused})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func fdDiagnosticFinalCount(window processharness.Window) int {
|
||||
if len(window.Samples) == 0 {
|
||||
return 0
|
||||
}
|
||||
return window.Samples[len(window.Samples)-1].NonStdioFDCount
|
||||
}
|
||||
|
||||
func fdDiagnosticIdentityMatches(candidate fdDiagnosticCandidate) bool {
|
||||
baseline := candidate.Baseline
|
||||
end := candidate.End
|
||||
return baseline.Process.PID > 0 && baseline.Process.PID == baseline.Identity.PID && baseline.Identity.PID == baseline.Window.PID && end.Process.PID > 0 && end.Process.PID == end.Identity.PID && end.Identity.PID == end.Window.PID && baseline.Process.PID == end.Process.PID && baseline.Identity.Generation != 0 && baseline.Identity.Generation == end.Identity.Generation
|
||||
}
|
||||
|
||||
func fdDiagnosticWindowFromProcess(window processharness.Window) *fdDiagnosticWindow {
|
||||
result := &fdDiagnosticWindow{PID: window.PID, Samples: make([]fdDiagnosticSample, 0, len(window.Samples))}
|
||||
for index, sample := range window.Samples {
|
||||
result.Samples = append(result.Samples, fdDiagnosticSample{Ordinal: index + 1, PID: sample.PID, RSSBytes: sample.RSSBytes, DescendantPIDs: append([]int(nil), sample.DescendantPIDs...), DescendantCount: sample.DescendantCount, NonStdioFDCount: sample.NonStdioFDCount, TCPListenerCount: sample.TCPListenerCount, TCP6ListenerCount: sample.TCP6ListenerCount, FDObservations: fdDiagnosticSortedObservations(sample.FDObservations), ObservedAt: sample.SampledAt})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func fdDiagnosticFinalObservations(window processharness.Window) []processharness.FDObservation {
|
||||
if len(window.Samples) == 0 {
|
||||
return nil
|
||||
}
|
||||
return fdDiagnosticSortedObservations(window.Samples[len(window.Samples)-1].FDObservations)
|
||||
}
|
||||
|
||||
func fdDiagnosticDifference(left, right []processharness.FDObservation) []processharness.FDObservation {
|
||||
rightSet := make(map[processharness.FDObservation]struct{}, len(right))
|
||||
for _, observation := range right {
|
||||
rightSet[observation] = struct{}{}
|
||||
}
|
||||
result := make([]processharness.FDObservation, 0)
|
||||
for _, observation := range left {
|
||||
if _, exists := rightSet[observation]; !exists {
|
||||
result = append(result, observation)
|
||||
}
|
||||
}
|
||||
return fdDiagnosticSortedObservations(result)
|
||||
}
|
||||
|
||||
func fdDiagnosticSortedObservations(input []processharness.FDObservation) []processharness.FDObservation {
|
||||
result := append([]processharness.FDObservation(nil), input...)
|
||||
sort.Slice(result, func(left, right int) bool {
|
||||
return result[left].Number < result[right].Number || (result[left].Number == result[right].Number && result[left].Target < result[right].Target)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func fdDiagnosticAllPresent(present []bool) bool {
|
||||
for _, value := range present {
|
||||
if !value {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func fdDiagnosticAnyPresent(present []bool) bool {
|
||||
for _, value := range present {
|
||||
if value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fdDiagnosticPrefixPresent(present []bool) bool {
|
||||
cleared := false
|
||||
for _, value := range present {
|
||||
if !value {
|
||||
cleared = true
|
||||
} else if cleared {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return cleared
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
//go:build linux && agentcompat
|
||||
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
|
||||
processharness "github.com/nezhahq/nezha/integration/agentcompat/internal/process"
|
||||
)
|
||||
|
||||
const fdDiagnosticTailSampleCount = 20
|
||||
|
||||
type fdDiagnosticSampleFunc func(context.Context, int) (processharness.Sample, error)
|
||||
|
||||
type fdDiagnosticLogger interface {
|
||||
Logf(string, ...any)
|
||||
}
|
||||
|
||||
type fdDiagnosticCollectorSpec struct {
|
||||
Enabled bool
|
||||
SamplerPID int
|
||||
TailResultCapacity int
|
||||
TailInterval time.Duration
|
||||
Sample fdDiagnosticSampleFunc
|
||||
}
|
||||
|
||||
type fdDiagnosticTailResult struct {
|
||||
AgentOrdinal int
|
||||
Samples []fdDiagnosticSample
|
||||
Err error
|
||||
}
|
||||
|
||||
type fdDiagnosticTailSpec struct {
|
||||
Context context.Context
|
||||
PID int
|
||||
Interval time.Duration
|
||||
Sample fdDiagnosticSampleFunc
|
||||
FirstSampleComplete chan<- struct{}
|
||||
}
|
||||
|
||||
type fdDiagnosticCollector struct {
|
||||
enabled bool
|
||||
samplerID int
|
||||
interval time.Duration
|
||||
sample fdDiagnosticSampleFunc
|
||||
baseline map[int]fdDiagnosticAgentWindow
|
||||
records map[int]fdDiagnosticRecord
|
||||
results chan fdDiagnosticTailResult
|
||||
started int
|
||||
waitOnce sync.Once
|
||||
logOnce sync.Once
|
||||
completed []fdDiagnosticRecord
|
||||
}
|
||||
|
||||
func newFDDiagnosticCollector(spec fdDiagnosticCollectorSpec) *fdDiagnosticCollector {
|
||||
capacity := spec.TailResultCapacity
|
||||
if capacity < contract.PRFullAgentCount {
|
||||
capacity = contract.PRFullAgentCount
|
||||
}
|
||||
return &fdDiagnosticCollector{
|
||||
enabled: spec.Enabled,
|
||||
samplerID: spec.SamplerPID,
|
||||
interval: spec.TailInterval,
|
||||
sample: spec.Sample,
|
||||
baseline: make(map[int]fdDiagnosticAgentWindow),
|
||||
records: make(map[int]fdDiagnosticRecord),
|
||||
results: make(chan fdDiagnosticTailResult, capacity),
|
||||
}
|
||||
}
|
||||
|
||||
func newRealFDDiagnosticCollector(enabled bool) *fdDiagnosticCollector {
|
||||
return newFDDiagnosticCollector(fdDiagnosticCollectorSpec{
|
||||
Enabled: enabled,
|
||||
SamplerPID: os.Getpid(),
|
||||
TailResultCapacity: contract.PRFullAgentCount,
|
||||
TailInterval: contract.ResourceSampleInterval,
|
||||
Sample: func(_ context.Context, pid int) (processharness.Sample, error) {
|
||||
return processharness.SampleProcessWithFDObservations(pid)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (collector *fdDiagnosticCollector) Enabled() bool { return collector != nil && collector.enabled }
|
||||
|
||||
func (collector *fdDiagnosticCollector) RecordBaseline(window fdDiagnosticAgentWindow) {
|
||||
if collector.Enabled() && window.Process.Kind == StressProcessAgent {
|
||||
collector.baseline[window.Process.Agent.Int()] = window
|
||||
}
|
||||
}
|
||||
|
||||
func (collector *fdDiagnosticCollector) RecordEnd(ctx context.Context, window fdDiagnosticAgentWindow) {
|
||||
if !collector.Enabled() || window.Process.Kind != StressProcessAgent {
|
||||
return
|
||||
}
|
||||
baseline, exists := collector.baseline[window.Process.Agent.Int()]
|
||||
if !exists || fdDiagnosticFinalCount(baseline.Window) == fdDiagnosticFinalCount(window.Window) {
|
||||
return
|
||||
}
|
||||
record, startTail := newFDDiagnosticRecord(fdDiagnosticCandidate{Baseline: baseline, End: window}, collector.samplerID)
|
||||
collector.records[record.AgentOrdinal] = record
|
||||
if !startTail {
|
||||
return
|
||||
}
|
||||
collector.started++
|
||||
firstSampleComplete := make(chan struct{})
|
||||
tailSpec := fdDiagnosticTailSpec{Context: ctx, PID: record.AgentPID, Interval: collector.interval, Sample: collector.sample, FirstSampleComplete: firstSampleComplete}
|
||||
go func(ordinal int) {
|
||||
result := collectFDDiagnosticTail(tailSpec)
|
||||
result.AgentOrdinal = ordinal
|
||||
collector.results <- result
|
||||
}(record.AgentOrdinal)
|
||||
select {
|
||||
case <-firstSampleComplete:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
func (collector *fdDiagnosticCollector) WaitRecords() []fdDiagnosticRecord {
|
||||
if !collector.Enabled() {
|
||||
return nil
|
||||
}
|
||||
collector.waitOnce.Do(func() {
|
||||
for range collector.started {
|
||||
result := <-collector.results
|
||||
record := collector.records[result.AgentOrdinal]
|
||||
record.Tail = result.Samples
|
||||
if result.Err != nil {
|
||||
record.DiagnosticError = result.Err.Error()
|
||||
record.LifecycleStatus = "tail_error"
|
||||
} else {
|
||||
record.Lifecycle = classifyFDDiagnosticLifecycle(record.AddedFinal, record.Tail)
|
||||
record.LifecycleStatus = "tail_complete"
|
||||
}
|
||||
collector.records[result.AgentOrdinal] = record
|
||||
}
|
||||
collector.completed = make([]fdDiagnosticRecord, 0, len(collector.records))
|
||||
for _, record := range collector.records {
|
||||
collector.completed = append(collector.completed, record)
|
||||
}
|
||||
sort.Slice(collector.completed, func(left, right int) bool {
|
||||
return collector.completed[left].AgentOrdinal < collector.completed[right].AgentOrdinal
|
||||
})
|
||||
})
|
||||
return append([]fdDiagnosticRecord(nil), collector.completed...)
|
||||
}
|
||||
|
||||
// The real collector is directly logger-injectable so tests exercise candidate selection and logging without shadow copies.
|
||||
func (collector *fdDiagnosticCollector) WaitAndLog(logger fdDiagnosticLogger) {
|
||||
if !collector.Enabled() {
|
||||
return
|
||||
}
|
||||
collector.logOnce.Do(func() {
|
||||
for _, record := range collector.WaitRecords() {
|
||||
encoded, err := json.Marshal(record)
|
||||
if err != nil {
|
||||
logger.Logf("agentcompat_fd_diagnostic={\"diagnostic_error\":%q}", err.Error())
|
||||
continue
|
||||
}
|
||||
logger.Logf("agentcompat_fd_diagnostic=%s", encoded)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func collectFDDiagnosticTail(spec fdDiagnosticTailSpec) fdDiagnosticTailResult {
|
||||
result := fdDiagnosticTailResult{Samples: make([]fdDiagnosticSample, 0, fdDiagnosticTailSampleCount)}
|
||||
signalFirstSampleComplete := func() {
|
||||
if spec.FirstSampleComplete != nil {
|
||||
close(spec.FirstSampleComplete)
|
||||
spec.FirstSampleComplete = nil
|
||||
}
|
||||
}
|
||||
for index := 0; index < fdDiagnosticTailSampleCount; index++ {
|
||||
if index > 0 {
|
||||
timer := time.NewTimer(spec.Interval)
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-spec.Context.Done():
|
||||
timer.Stop()
|
||||
result.Err = spec.Context.Err()
|
||||
return result
|
||||
}
|
||||
}
|
||||
if err := spec.Context.Err(); err != nil {
|
||||
signalFirstSampleComplete()
|
||||
result.Err = err
|
||||
return result
|
||||
}
|
||||
processSample, err := spec.Sample(spec.Context, spec.PID)
|
||||
if err != nil {
|
||||
signalFirstSampleComplete()
|
||||
result.Err = err
|
||||
return result
|
||||
}
|
||||
result.Samples = append(result.Samples, fdDiagnosticSampleFromProcess(index+1, processSample))
|
||||
signalFirstSampleComplete()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func fdDiagnosticSampleFromProcess(ordinal int, sample processharness.Sample) fdDiagnosticSample {
|
||||
return fdDiagnosticSample{Ordinal: ordinal, PID: sample.PID, RSSBytes: sample.RSSBytes, DescendantPIDs: append([]int(nil), sample.DescendantPIDs...), DescendantCount: sample.DescendantCount, NonStdioFDCount: sample.NonStdioFDCount, TCPListenerCount: sample.TCPListenerCount, TCP6ListenerCount: sample.TCP6ListenerCount, FDObservations: fdDiagnosticSortedObservations(sample.FDObservations), ObservedAt: sample.SampledAt}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
//go:build linux && agentcompat
|
||||
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/agent"
|
||||
processharness "github.com/nezhahq/nezha/integration/agentcompat/internal/process"
|
||||
)
|
||||
|
||||
func TestFDDiagnosticEnabled_OnlyAcceptsExactOne(t *testing.T) {
|
||||
for _, value := range []string{"", "0", "true", "01", " 1", "1 ", "\t1"} {
|
||||
require.False(t, fdDiagnosticEnabled(value), "value=%q", value)
|
||||
}
|
||||
require.True(t, fdDiagnosticEnabled("1"))
|
||||
}
|
||||
|
||||
func TestFDDiagnosticCandidates_ExcludeDashboardAndMatchOrdinals(t *testing.T) {
|
||||
collector := newFDDiagnosticCollector(fdDiagnosticCollectorSpec{Enabled: true, TailResultCapacity: 8, Sample: fdDiagnosticTestSampler})
|
||||
agentOne := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 9, Target: "one"})
|
||||
agentTwo := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 2, PID: 102, BaselineCount: 8, EndCount: 8, Target: "two"})
|
||||
dashboard := stressDiagnosticDashboardWindow(t, stressDiagnosticDashboardWindowSpec{PID: 100, BaselineCount: 8, EndCount: 9})
|
||||
collector.RecordBaseline(dashboard.baseline)
|
||||
collector.RecordBaseline(agentTwo.baseline)
|
||||
collector.RecordBaseline(agentOne.baseline)
|
||||
|
||||
collector.RecordEnd(t.Context(), agentTwo.end)
|
||||
collector.RecordEnd(t.Context(), dashboard.end)
|
||||
collector.RecordEnd(t.Context(), agentOne.end)
|
||||
records := collector.WaitRecords()
|
||||
|
||||
require.Len(t, records, 1)
|
||||
require.Equal(t, 1, records[0].AgentOrdinal)
|
||||
require.Equal(t, 101, records[0].AgentPID)
|
||||
}
|
||||
|
||||
func TestFDDiagnosticCandidates_UseOnlyFinalSampleCounts(t *testing.T) {
|
||||
collector := newFDDiagnosticCollector(fdDiagnosticCollectorSpec{Enabled: true, TailResultCapacity: 8, Sample: fdDiagnosticTestSampler})
|
||||
pair := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 8, Target: "stable"})
|
||||
pair.end.Window.Samples[0].NonStdioFDCount = 9
|
||||
collector.RecordBaseline(pair.baseline)
|
||||
|
||||
collector.RecordEnd(t.Context(), pair.end)
|
||||
require.Empty(t, collector.WaitRecords())
|
||||
|
||||
pair.end.Window.Samples[len(pair.end.Window.Samples)-1].NonStdioFDCount = 9
|
||||
collector = newFDDiagnosticCollector(fdDiagnosticCollectorSpec{Enabled: true, TailResultCapacity: 8, Sample: fdDiagnosticTestSampler})
|
||||
collector.RecordBaseline(pair.baseline)
|
||||
collector.RecordEnd(t.Context(), pair.end)
|
||||
require.Len(t, collector.WaitRecords(), 1)
|
||||
}
|
||||
|
||||
func TestFDDiagnosticRecord_ReportsIdentityChangeWithoutTail(t *testing.T) {
|
||||
pair := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 9, Target: "changed"})
|
||||
pair.end.Identity = agent.ProcessIdentity{Generation: 2, PID: 202}
|
||||
candidate := fdDiagnosticCandidate{Baseline: pair.baseline, End: pair.end}
|
||||
|
||||
record, tail := newFDDiagnosticRecord(candidate, 999)
|
||||
|
||||
require.Equal(t, "process_identity_changed", record.LifecycleStatus)
|
||||
require.False(t, tail)
|
||||
require.Empty(t, record.Lifecycle)
|
||||
}
|
||||
|
||||
func TestFDDiagnosticRecord_RejectsStressProcessPIDMismatch(t *testing.T) {
|
||||
pair := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 9, Target: "process-pid-mismatch"})
|
||||
pair.end.Process.PID = 202
|
||||
candidate := fdDiagnosticCandidate{Baseline: pair.baseline, End: pair.end}
|
||||
|
||||
record, tail := newFDDiagnosticRecord(candidate, 999)
|
||||
|
||||
require.Equal(t, "process_identity_changed", record.LifecycleStatus)
|
||||
require.False(t, tail)
|
||||
}
|
||||
|
||||
func TestFDDiagnosticRecord_RejectsZeroRuntimeGeneration(t *testing.T) {
|
||||
pair := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 9, Target: "zero-generation"})
|
||||
pair.end.Identity.Generation = 0
|
||||
candidate := fdDiagnosticCandidate{Baseline: pair.baseline, End: pair.end}
|
||||
|
||||
record, tail := newFDDiagnosticRecord(candidate, 999)
|
||||
|
||||
require.Equal(t, "process_identity_changed", record.LifecycleStatus)
|
||||
require.False(t, tail)
|
||||
}
|
||||
|
||||
func TestFDDiagnosticRecord_EncodesStableObservedAtForEveryWindowSample(t *testing.T) {
|
||||
pair := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 9, Target: "timestamps"})
|
||||
baselineTime := time.Date(2025, time.January, 2, 3, 4, 5, 0, time.UTC)
|
||||
endTime := baselineTime.Add(time.Minute)
|
||||
for index := range pair.baseline.Window.Samples {
|
||||
pair.baseline.Window.Samples[index].SampledAt = baselineTime.Add(time.Duration(index) * time.Second)
|
||||
pair.end.Window.Samples[index].SampledAt = endTime.Add(time.Duration(index) * time.Second)
|
||||
}
|
||||
candidate := fdDiagnosticCandidate{Baseline: pair.baseline, End: pair.end}
|
||||
|
||||
record, tail := newFDDiagnosticRecord(candidate, 999)
|
||||
encoded, err := json.Marshal(record)
|
||||
|
||||
require.True(t, tail)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, baselineTime, record.Baseline.Samples[0].ObservedAt)
|
||||
require.Equal(t, endTime, record.End.Samples[0].ObservedAt)
|
||||
require.Contains(t, string(encoded), `"observed_at":"2025-01-02T03:04:05Z"`)
|
||||
}
|
||||
|
||||
func TestFDDiagnosticRecord_DiffsFinalObservationsByExactIdentity(t *testing.T) {
|
||||
pair := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 9, Target: "diff"})
|
||||
pair.baseline.Window.Samples[4].FDObservations = []processharness.FDObservation{{Number: 5, Target: "beta"}, {Number: 4, Target: "alpha"}}
|
||||
pair.end.Window.Samples[4].FDObservations = []processharness.FDObservation{{Number: 6, Target: "gamma"}, {Number: 5, Target: "beta"}}
|
||||
candidate := fdDiagnosticCandidate{Baseline: pair.baseline, End: pair.end}
|
||||
|
||||
record, tail := newFDDiagnosticRecord(candidate, 999)
|
||||
|
||||
require.True(t, tail)
|
||||
require.Equal(t, []processharness.FDObservation{{Number: 6, Target: "gamma"}}, record.AddedFinal)
|
||||
require.Equal(t, []processharness.FDObservation{{Number: 4, Target: "alpha"}}, record.RemovedFinal)
|
||||
}
|
||||
|
||||
func TestFDDiagnosticLifecycle_ClassifiesEachFinalAddition(t *testing.T) {
|
||||
added := []processharness.FDObservation{{Number: 3, Target: "before"}, {Number: 4, Target: "during"}, {Number: 5, Target: "through"}, {Number: 6, Target: "intermittent"}, {Number: 7, Target: "reused"}}
|
||||
tail := []fdDiagnosticSample{
|
||||
stressDiagnosticSample(1, []processharness.FDObservation{{Number: 4, Target: "during"}, {Number: 5, Target: "through"}, {Number: 6, Target: "intermittent"}, {Number: 7, Target: "reused"}}),
|
||||
stressDiagnosticSample(2, []processharness.FDObservation{{Number: 5, Target: "through"}, {Number: 7, Target: "other"}}),
|
||||
stressDiagnosticSample(3, []processharness.FDObservation{{Number: 5, Target: "through"}, {Number: 6, Target: "intermittent"}, {Number: 7, Target: "reused"}}),
|
||||
}
|
||||
|
||||
lifecycle := classifyFDDiagnosticLifecycle(added, tail)
|
||||
|
||||
require.Equal(t, []fdDiagnosticLifecycle{
|
||||
{Observation: processharness.FDObservation{Number: 3, Target: "before"}, Status: "cleared_before_tail"},
|
||||
{Observation: processharness.FDObservation{Number: 4, Target: "during"}, Status: "cleared_during_tail"},
|
||||
{Observation: processharness.FDObservation{Number: 5, Target: "through"}, Status: "observed_through_tail"},
|
||||
{Observation: processharness.FDObservation{Number: 6, Target: "intermittent"}, Status: "intermittent_or_reused"},
|
||||
{Observation: processharness.FDObservation{Number: 7, Target: "reused"}, Status: "intermittent_or_reused", NumberReused: true},
|
||||
}, lifecycle)
|
||||
}
|
||||
|
||||
func TestFDDiagnosticFields_DoNotChangeStressEvaluationOrEvidenceJSON(t *testing.T) {
|
||||
resource := stressDashboardResourceFixture(100)
|
||||
beforeEvaluation, err := EvaluateStressResource(resource)
|
||||
require.NoError(t, err)
|
||||
for sampleIndex := range resource.Baseline.Samples {
|
||||
resource.Baseline.Samples[sampleIndex].FDObservations = []processharness.FDObservation{{Number: 9, Target: "diagnostic"}}
|
||||
resource.End.Samples[sampleIndex].FDObservations = []processharness.FDObservation{{Number: 9, Target: "diagnostic"}}
|
||||
}
|
||||
afterEvaluation, err := EvaluateStressResource(resource)
|
||||
require.NoError(t, err)
|
||||
profile := mustStressPRFullProfile(t)
|
||||
evidence := validStressEvidence(t, profile)
|
||||
beforeEvidence, err := json.Marshal(evidence)
|
||||
require.NoError(t, err)
|
||||
for iteration := range evidence.Iterations {
|
||||
for resourceIndex := range evidence.Iterations[iteration].Resources {
|
||||
for sampleIndex := range evidence.Iterations[iteration].Resources[resourceIndex].Baseline.Samples {
|
||||
evidence.Iterations[iteration].Resources[resourceIndex].Baseline.Samples[sampleIndex].FDObservations = []processharness.FDObservation{{Number: 9, Target: "diagnostic"}}
|
||||
evidence.Iterations[iteration].Resources[resourceIndex].End.Samples[sampleIndex].FDObservations = []processharness.FDObservation{{Number: 9, Target: "diagnostic"}}
|
||||
}
|
||||
}
|
||||
}
|
||||
afterEvidence, err := json.Marshal(evidence)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, beforeEvaluation, afterEvaluation)
|
||||
require.Equal(t, string(beforeEvidence), string(afterEvidence))
|
||||
}
|
||||
Reference in New Issue
Block a user