Frontend Labs
⋯

Day 110 — Chaos Engineering

Learning objective Build configurable fault injection and a circuit breaker
Key packages net/http, math/rand, sync/atomic, time
Security context Chaos testing exposes brittleness in security middleware before production does

Exercise — Chaos Middleware + Circuit Breaker

package chaos

import (
    "math/rand"
    "net/http"
    "sync/atomic"
    "time"
)

// ChaosConfig controls fault injection rates (all zero by default — safe to deploy)
type ChaosConfig struct {
    ErrorRate   float64       // 0.0–1.0: probability of returning 500
    DelayRate   float64       // 0.0–1.0: probability of injecting delay
    MinDelay    time.Duration
    MaxDelay    time.Duration
    CloseRate   float64       // 0.0–1.0: probability of hijacking and closing connection
    Enabled     atomic.Bool   // master kill switch
}

func NewChaosConfig() *ChaosConfig {
    cfg := &ChaosConfig{
        ErrorRate: 0.05, MaxDelay: 2 * time.Second, MinDelay: 100 * time.Millisecond,
        DelayRate: 0.02, CloseRate: 0.01,
    }
    // Disabled by default — enable via CHAOS_ENABLED env var or config
    return cfg
}

// ChaosMiddleware injects random faults per ChaosConfig
func ChaosMiddleware(cfg *ChaosConfig, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !cfg.Enabled.Load() {
            next.ServeHTTP(w, r)
            return
        }

        roll := rand.Float64()

        // Error injection
        if roll < cfg.ErrorRate {
            http.Error(w, `{"error":"chaos_injected"}`, http.StatusInternalServerError)
            return
        }

        // Latency injection
        if roll < cfg.ErrorRate+cfg.DelayRate {
            delay := cfg.MinDelay + time.Duration(rand.Int63n(int64(cfg.MaxDelay-cfg.MinDelay)))
            time.Sleep(delay)
        }

        // Connection close injection — requires Hijacker
        if roll < cfg.ErrorRate+cfg.DelayRate+cfg.CloseRate {
            if h, ok := w.(http.Hijacker); ok {
                conn, _, err := h.Hijack()
                if err == nil {
                    conn.Close()
                    return
                }
            }
        }

        next.ServeHTTP(w, r)
    })
}

// CircuitBreaker — 3 states: closed (normal), open (blocking), half-open (testing)
type CircuitBreaker struct {
    failures    atomic.Int64
    successes   atomic.Int64
    state       atomic.Int32 // 0=closed, 1=open, 2=half-open
    lastFailure atomic.Int64 // unix nano

    failureThreshold int64
    successThreshold int64
    openDuration     time.Duration
    halfOpenAllowed  atomic.Int32 // permit count in half-open state
}

const (
    stateClosed   int32 = 0
    stateOpen     int32 = 1
    stateHalfOpen int32 = 2
)

func NewCircuitBreaker(failureThreshold, successThreshold int, openDuration time.Duration) *CircuitBreaker {
    return &CircuitBreaker{
        failureThreshold: int64(failureThreshold),
        successThreshold: int64(successThreshold),
        openDuration:     openDuration,
    }
}

// Allow returns true if the request should proceed
func (cb *CircuitBreaker) Allow() bool {
    switch cb.state.Load() {
    case stateClosed:
        return true

    case stateOpen:
        // Check if enough time has passed to try half-open
        lastFail := time.Unix(0, cb.lastFailure.Load())
        if time.Since(lastFail) > cb.openDuration {
            if cb.state.CompareAndSwap(stateOpen, stateHalfOpen) {
                cb.halfOpenAllowed.Store(1) // allow one request to test
            }
            return cb.state.Load() == stateHalfOpen && cb.halfOpenAllowed.Add(-1) >= 0
        }
        return false

    case stateHalfOpen:
        return cb.halfOpenAllowed.Add(-1) >= 0
    }
    return true
}

// RecordSuccess records a successful request
func (cb *CircuitBreaker) RecordSuccess() {
    if cb.state.Load() == stateHalfOpen {
        if cb.successes.Add(1) >= cb.successThreshold {
            cb.successes.Store(0)
            cb.failures.Store(0)
            cb.state.Store(stateClosed)
        }
    }
}

// RecordFailure records a failed request
func (cb *CircuitBreaker) RecordFailure() {
    cb.lastFailure.Store(time.Now().UnixNano())
    if cb.failures.Add(1) >= cb.failureThreshold {
        cb.state.Store(stateOpen)
        cb.failures.Store(0)
        cb.successes.Store(0)
    }
}

func (cb *CircuitBreaker) State() string {
    switch cb.state.Load() {
    case stateClosed:
        return "closed"
    case stateOpen:
        return "open"
    case stateHalfOpen:
        return "half_open"
    }
    return "unknown"
}

// Middleware wraps an HTTP handler with circuit breaker protection
func (cb *CircuitBreaker) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !cb.Allow() {
            http.Error(w, `{"error":"circuit_open"}`, http.StatusServiceUnavailable)
            return
        }

        // Use statusCapture to detect failures
        sc := &statusCapture{ResponseWriter: w}
        next.ServeHTTP(sc, r)

        if sc.status >= 500 {
            cb.RecordFailure()
        } else {
            cb.RecordSuccess()
        }
    })
}

type statusCapture struct {
    http.ResponseWriter
    status int
}

func (sc *statusCapture) WriteHeader(code int) {
    sc.status = code
    sc.ResponseWriter.WriteHeader(code)
}

Generated from week15.md · 8 Oct 2026 16:36