feat: add admin console, Redis broker and chaos lab for training demo
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/jkaninda/logger"
|
||||
"github.com/jkaninda/okapi"
|
||||
)
|
||||
|
||||
// Failure injection so restarts, health gating, analytics and alerts have
|
||||
// something to react to during a demo. See DEMO.md.
|
||||
|
||||
const (
|
||||
maxSlow = 30 * time.Second
|
||||
maxBurn = 120 * time.Second
|
||||
maxBallastMB = 2048
|
||||
maxLogBurst = 1000
|
||||
defaultBurnS = 30
|
||||
defaultMemMB = 256
|
||||
)
|
||||
|
||||
type chaosState struct {
|
||||
cpuBusy atomic.Bool
|
||||
ballastMB atomic.Int64
|
||||
}
|
||||
|
||||
// RequireDebug rejects requests unless DEBUG_ENDPOINTS=true.
|
||||
func (h *Handler) RequireDebug(c *okapi.Context) error {
|
||||
if !h.debug {
|
||||
return c.JSON(http.StatusNotFound, okapi.M{"error": "debug endpoints are disabled (set DEBUG_ENDPOINTS=true)"})
|
||||
}
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// DebugCrash exits the process with status 1 after answering.
|
||||
func (h *Handler) DebugCrash(c *okapi.Context) error {
|
||||
logger.Error("crash requested via /api/debug/crash, exiting", "host", h.host)
|
||||
go func() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
os.Exit(1)
|
||||
}()
|
||||
return c.JSON(http.StatusAccepted, okapi.M{"status": "crashing", "host": h.host})
|
||||
}
|
||||
|
||||
// DebugSlow sleeps for ?ms= milliseconds (default 2000) before answering.
|
||||
func (h *Handler) DebugSlow(c *okapi.Context) error {
|
||||
d := min(time.Duration(atoiDefault(c.Query("ms"), 2000))*time.Millisecond, maxSlow)
|
||||
select {
|
||||
case <-time.After(d):
|
||||
case <-c.Request().Context().Done():
|
||||
return nil
|
||||
}
|
||||
return c.OK(okapi.M{"slept_ms": d.Milliseconds(), "host": h.host})
|
||||
}
|
||||
|
||||
// DebugError answers with ?status= (default 500).
|
||||
func (h *Handler) DebugError(c *okapi.Context) error {
|
||||
status := atoiDefault(c.Query("status"), http.StatusInternalServerError)
|
||||
if status < 400 || status > 599 {
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
logger.Error("injected error via /api/debug/error", "status", status, "host", h.host)
|
||||
return c.JSON(status, okapi.M{"error": "injected failure", "status": status, "host": h.host})
|
||||
}
|
||||
|
||||
// DebugHealth forces /healthz on this replica to fail (or recover).
|
||||
func (h *Handler) DebugHealth(c *okapi.Context) error {
|
||||
var req struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
}
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, okapi.M{"error": "invalid JSON body"})
|
||||
}
|
||||
h.unhealthy.Store(!req.Healthy)
|
||||
logger.Warn("health overridden via /api/debug/health", "healthy", req.Healthy, "host", h.host)
|
||||
return c.OK(okapi.M{"healthy": req.Healthy, "host": h.host})
|
||||
}
|
||||
|
||||
// DebugCPU saturates every core for ?seconds= (default 30).
|
||||
func (h *Handler) DebugCPU(c *okapi.Context) error {
|
||||
d := min(time.Duration(atoiDefault(c.Query("seconds"), defaultBurnS))*time.Second, maxBurn)
|
||||
if !h.chaos.cpuBusy.CompareAndSwap(false, true) {
|
||||
return c.JSON(http.StatusConflict, okapi.M{"error": "a CPU burn is already running"})
|
||||
}
|
||||
workers := runtime.GOMAXPROCS(0)
|
||||
deadline := time.Now().Add(d)
|
||||
done := make(chan struct{}, workers)
|
||||
for range workers {
|
||||
go func() {
|
||||
x := 0
|
||||
for time.Now().Before(deadline) {
|
||||
for i := range 1_000_000 {
|
||||
x += i
|
||||
}
|
||||
}
|
||||
_ = x
|
||||
done <- struct{}{}
|
||||
}()
|
||||
}
|
||||
go func() {
|
||||
for range workers {
|
||||
<-done
|
||||
}
|
||||
h.chaos.cpuBusy.Store(false)
|
||||
logger.Info("cpu burn finished", "host", h.host)
|
||||
}()
|
||||
logger.Warn("cpu burn started", "seconds", d.Seconds(), "workers", workers, "host", h.host)
|
||||
return c.JSON(http.StatusAccepted, okapi.M{"seconds": d.Seconds(), "workers": workers, "host": h.host})
|
||||
}
|
||||
|
||||
// DebugMemory holds ?mb= MiB (default 256) of touched memory for ?seconds=
|
||||
// (default 30). Useful to show memory limits and OOM restarts.
|
||||
func (h *Handler) DebugMemory(c *okapi.Context) error {
|
||||
mb := min(atoiDefault(c.Query("mb"), defaultMemMB), maxBallastMB)
|
||||
d := min(time.Duration(atoiDefault(c.Query("seconds"), defaultBurnS))*time.Second, maxBurn)
|
||||
if mb <= 0 {
|
||||
return c.JSON(http.StatusBadRequest, okapi.M{"error": "mb must be positive"})
|
||||
}
|
||||
if !h.chaos.ballastMB.CompareAndSwap(0, int64(mb)) {
|
||||
return c.JSON(http.StatusConflict, okapi.M{"error": "memory is already being held"})
|
||||
}
|
||||
go func() {
|
||||
ballast := make([]byte, mb<<20)
|
||||
// Touch every page so the memory is actually resident, not just reserved.
|
||||
for i := 0; i < len(ballast); i += 4096 {
|
||||
ballast[i] = 1
|
||||
}
|
||||
time.Sleep(d)
|
||||
runtime.KeepAlive(ballast)
|
||||
h.chaos.ballastMB.Store(0)
|
||||
logger.Info("memory released", "mb", mb, "host", h.host)
|
||||
}()
|
||||
logger.Warn("holding memory", "mb", mb, "seconds", d.Seconds(), "host", h.host)
|
||||
return c.JSON(http.StatusAccepted, okapi.M{"mb": mb, "seconds": d.Seconds(), "host": h.host})
|
||||
}
|
||||
|
||||
// DebugLogs writes ?count= (default 20) log lines at ?level= (default error).
|
||||
func (h *Handler) DebugLogs(c *okapi.Context) error {
|
||||
count := min(max(atoiDefault(c.Query("count"), 20), 1), maxLogBurst)
|
||||
level := c.Query("level")
|
||||
for i := range count {
|
||||
n := strconv.Itoa(i + 1)
|
||||
switch level {
|
||||
case "info":
|
||||
logger.Info("demo log line", "n", n, "host", h.host)
|
||||
case "warn":
|
||||
logger.Warn("demo log line", "n", n, "host", h.host)
|
||||
default:
|
||||
level = "error"
|
||||
logger.Error("demo log line", "n", n, "host", h.host)
|
||||
}
|
||||
}
|
||||
return c.OK(okapi.M{"count": count, "level": level, "host": h.host})
|
||||
}
|
||||
Reference in New Issue
Block a user