//go:build integration package monitoring import ( "context" "fmt" "math/rand" "strings" "sync" "time" internalerrors "github.com/rcourtman/pulse-go-rewrite/internal/errors" ) type fakeExecutor struct { monitor *Monitor configs map[string]InstanceConfig mu sync.Mutex state map[string]*instanceState rng *rand.Rand } type instanceState struct { config InstanceConfig seqIndex int successes int failures int transient int permanent int totalLatency time.Duration executions int lastError string lastSuccess time.Time } func newFakeExecutor(m *Monitor, scenario HarnessScenario) *fakeExecutor { cfgs := make(map[string]InstanceConfig, len(scenario.Instances)) for _, inst := range scenario.Instances { key := instanceKey(inst.Type, inst.Name) cfgs[key] = inst } return &fakeExecutor{ monitor: m, configs: cfgs, state: make(map[string]*instanceState, len(cfgs)), rng: rand.New(rand.NewSource(time.Now().UnixNano())), } } func (f *fakeExecutor) Execute(ctx context.Context, task PollTask) { start := time.Now() key := instanceKey(task.InstanceType, task.InstanceName) cfg, found := f.configs[key] if !found { cfg = InstanceConfig{ Type: task.InstanceType, Name: task.InstanceName, SuccessRate: 1.0, } } state := f.getState(key, cfg) latency := f.latencyFor(cfg) select { case <-ctx.Done(): return case <-time.After(latency): } failType := f.nextFailure(state, cfg) success := failType == FailureNone var pollErr error if !success { err := fmt.Errorf("synthetic failure on %s", task.InstanceName) switch failType { case FailureTransient: pollErr = internalerrors.NewMonitorError(internalerrors.ErrorTypeConnection, "fake_poll", task.InstanceName, err) case FailurePermanent: pollErr = internalerrors.NewMonitorError(internalerrors.ErrorTypeValidation, "fake_poll", task.InstanceName, err) default: pollErr = internalerrors.NewMonitorError(internalerrors.ErrorTypeInternal, "fake_poll", task.InstanceName, err) } } result := PollResult{ InstanceName: task.InstanceName, InstanceType: task.InstanceType, Success: success, Error: pollErr, StartTime: start, EndTime: time.Now(), } if f.monitor.pollMetrics != nil { f.monitor.pollMetrics.RecordResult(result) } instanceType := toInstanceType(task.InstanceType) if f.monitor.stalenessTracker != nil { if success { f.monitor.stalenessTracker.UpdateSuccess(instanceType, task.InstanceName, nil) } else { f.monitor.stalenessTracker.UpdateError(instanceType, task.InstanceName) } } f.monitor.recordTaskResult(instanceType, task.InstanceName, pollErr) f.recordStats(state, latency, success, failType, pollErr) } func (f *fakeExecutor) InstanceReport() map[string]InstanceStats { f.mu.Lock() defer f.mu.Unlock() report := make(map[string]InstanceStats, len(f.state)) for key, st := range f.state { avgLatency := time.Duration(0) if st.executions > 0 { avgLatency = st.totalLatency / time.Duration(st.executions) } report[key] = InstanceStats{ Total: st.executions, Successes: st.successes, Failures: st.failures, TransientFailures: st.transient, PermanentFailures: st.permanent, AverageLatency: avgLatency, LastError: st.lastError, LastSuccessAt: st.lastSuccess, } } return report } func (f *fakeExecutor) getState(key string, cfg InstanceConfig) *instanceState { f.mu.Lock() defer f.mu.Unlock() if st, ok := f.state[key]; ok { return st } st := &instanceState{config: cfg} f.state[key] = st return st } func (f *fakeExecutor) latencyFor(cfg InstanceConfig) time.Duration { base := cfg.BaseLatency if base <= 0 { base = 200 * time.Millisecond } jitter := base / 5 if jitter <= 0 { return base } offset := time.Duration(f.rng.Int63n(int64(jitter))) - jitter/2 return base + offset } func (f *fakeExecutor) nextFailure(state *instanceState, cfg InstanceConfig) FailureType { if state.seqIndex < len(cfg.FailureSeq) { ft := cfg.FailureSeq[state.seqIndex] state.seqIndex++ return ft } successRate := cfg.SuccessRate if successRate <= 0 { return FailureTransient } if successRate >= 1 { return FailureNone } if f.rng.Float64() <= successRate { return FailureNone } return FailureTransient } func (f *fakeExecutor) recordStats(state *instanceState, latency time.Duration, success bool, failure FailureType, pollErr error) { f.mu.Lock() defer f.mu.Unlock() state.executions++ state.totalLatency += latency if success { state.successes++ state.lastError = "" state.lastSuccess = time.Now() return } state.failures++ if failure == FailureTransient { state.transient++ } else if failure == FailurePermanent { state.permanent++ } if pollErr != nil { state.lastError = pollErr.Error() } } func instanceKey(typ, name string) string { return fmt.Sprintf("%s::%s", strings.ToLower(typ), name) }