feat: implement killer demo features — crash recovery, HMAC, dedup, benchmarks
ober
14031f65a1d2e612c5edbecabffa538da8054c4b
--- a/Makefile +++ b/Makefile @@ -21,6 +21,14 @@ run-custom: test: ./test-edge.sh +## Run with HMAC enabled for signature tests +test-hmac: + EDGE_SECRET=test-secret ./test-edge.sh + +## Benchmark with wrk (requires wrk) +bench: + ./bench-edge.sh + ## Syntax check (no server needed) check: LD_LIBRARY_PATH=$(NATIVE) $(SCHEME) --compile-imported-libraries --script /dev/null 2>&1 || true new file mode 100755 --- /dev/null +++ b/bench-edge.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# bench-edge.sh — Benchmark Jerboa Edge throughput +# +# Requires: wrk (apt install wrk / brew install wrk) +# Usage: Start the server first (make run), then run this script. + +set -euo pipefail + +PORT=${EDGE_PORT:-8080} +BASE="http://localhost:${PORT}" +DURATION=${BENCH_DURATION:-10s} +THREADS=${BENCH_THREADS:-4} +CONNECTIONS=${BENCH_CONNECTIONS:-100} + +# Verify server is up +if ! curl -sf "$BASE/health" > /dev/null 2>&1; then + echo "Error: Server not responding at $BASE/health" + echo "Start with: make run" + exit 1 +fi + +echo "═══════════════════════════════════════════════" +echo " Jerboa Edge Benchmark" +echo "═══════════════════════════════════════════════" +echo " target: $BASE" +echo " duration: $DURATION" +echo " threads: $THREADS" +echo " connections: $CONNECTIONS" +echo "" + +# Create Lua script for POST requests with unique IDs +cat > /tmp/edge-bench-post.lua << 'LUAEOF' +local counter = 0 + +request = function() + counter = counter + 1 + local body = string.format('{"id":"bench_%d_%d","amount":%d}', + counter, math.random(1000000), math.random(10000)) + return wrk.format("POST", "/hooks/payment.completed", + {["Content-Type"] = "application/json"}, body) +end +LUAEOF + +# ── GET /health (baseline) ──────────────────────────────────── +echo "--- GET /health (baseline) ---" +wrk -t"$THREADS" -c"$CONNECTIONS" -d"$DURATION" "$BASE/health" +echo "" + +# ── POST /hooks/:type (webhook ingestion) ───────────────────── +echo "--- POST /hooks/payment.completed ---" +wrk -t"$THREADS" -c"$CONNECTIONS" -d"$DURATION" -s /tmp/edge-bench-post.lua "$BASE/hooks/payment.completed" +echo "" + +# ── GET /api/stats (read path) ──────────────────────────────── +echo "--- GET /api/stats ---" +wrk -t"$THREADS" -c"$CONNECTIONS" -d"$DURATION" "$BASE/api/stats" +echo "" + +# Cleanup +rm -f /tmp/edge-bench-post.lua + +# Final stats +echo "═══════════════════════════════════════════════" +echo " Post-benchmark stats:" +curl -s "$BASE/api/stats" | python3 -m json.tool 2>/dev/null || curl -s "$BASE/api/stats" +echo "" +echo "═══════════════════════════════════════════════" --- a/edge.ss +++ b/edge.ss @@ -96,14 +96,25 @@ ;; HMAC-SHA256 Signature Verification ;; ═══════════════════════════════════════════════════════════════ +(def (bytes->hex bv) + (let ([out (make-string (* 2 (bytevector-length bv)))]) + (do ([i 0 (+ i 1)]) + ((= i (bytevector-length bv)) out) + (let* ([b (bytevector-u8-ref bv i)] + [hi (fxsrl b 4)] + [lo (fxand b #xf)]) + (string-set! out (* i 2) (string-ref "0123456789abcdef" hi)) + (string-set! out (+ (* i 2) 1) (string-ref "0123456789abcdef" lo)))))) + (def (verify-signature sig body) (or (string=? *secret* "") ;; skip when no secret configured (and sig - (let ([expected (native-hmac-sha256 - (string->utf8 *secret*) - (string->utf8 body))]) + (let ([expected (bytes->hex + (native-hmac-sha256 + (string->utf8 *secret*) + (string->utf8 body)))]) (native-crypto-memcmp - expected + (string->utf8 expected) (string->utf8 sig)))))) ;; ═══════════════════════════════════════════════════════════════ @@ -157,6 +168,13 @@ (lambda (evt) (displayln "[order] " (hash-ref evt "id") " shipped"))) +;; Crash handler — deliberately errors to demonstrate supervisor restart. +;; POST /hooks/test.crash to trigger. The worker that picks this up +;; will crash; the supervisor restarts it, other workers are unaffected. +(register-handler! "test.crash" + (lambda (evt) + (error 'worker "deliberate crash for demo"))) + (def (dispatch-handler type) (or (hash-get handler-registry type) (lambda (evt) @@ -169,6 +187,8 @@ ;; No intermediate allocations — compose-transducers fuses steps. ;; ═══════════════════════════════════════════════════════════════ +(def seen-ids (make-hash-table)) + (def ingest-xf (compose-transducers ;; Validate: drop malformed events @@ -176,6 +196,15 @@ (lambda (evt) (and (hash-key? evt "type") (hash-key? evt "payload")))) + ;; Deduplicate: drop events with IDs we've already seen + (filtering + (lambda (evt) + (let ([id (hash-ref evt "id" #f)]) + (cond + [(not id) #t] ;; no ID → pass through + [(hash-key? seen-ids id) + (displayln "[dedup] dropping duplicate " id) #f] + [else (hash-put! seen-ids id #t) #t])))) ;; Normalize: ensure every event has a timestamp (mapping (lambda (evt) @@ -207,14 +236,21 @@ (when (and event (not (eof-object? event))) (let* ([type (hash-ref event "type" "unknown")] [handler (dispatch-handler type)] - [status (guard (exn [#t - (displayln "[worker-" id "] error: " - (if (message-condition? exn) - (condition-message exn) exn)) - "error"]) - (handler event) "ok")]) + [result (try (ok (handler event)) + (catch (e) (err e)))] + [status (if (ok? result) "ok" "error")]) + (when (err? result) + (let ([e (unwrap-err result)]) + (displayln "[worker-" id "] error: " + (if (message-condition? e) + (condition-message e) e)))) (store-result! event status) - (notify-watchers! event status)) + (notify-watchers! event status) + ;; Deliberate crash: re-raise to kill actor. + ;; Supervisor will restart it automatically. + (when (and (err? result) (string=? type "test.crash")) + (displayln "[worker-" id "] crashing — supervisor will restart") + (raise (unwrap-err result)))) (loop))))] [_ (void)]))) --- a/test-edge.sh +++ b/test-edge.sh @@ -2,6 +2,7 @@ # test-edge.sh — Smoke tests for Jerboa Edge # # Usage: Start the server first (make run), then run this script. +# For HMAC tests, start with: EDGE_SECRET=test-secret make run set -euo pipefail @@ -23,16 +24,27 @@ check() { fi } +check_code() { + local name="$1" expected="$2" actual="$3" + if [ "$actual" = "$expected" ]; then + echo " PASS: $name" + PASS=$((PASS + 1)) + else + echo " FAIL: $name (expected HTTP $expected, got $actual)" + FAIL=$((FAIL + 1)) + fi +} + echo "=== Jerboa Edge Smoke Tests ===" echo " target: $BASE" echo "" -# 1. Health check +# ── 1. Health check ────────────────────────────────────────── echo "--- Health ---" R=$(curl -s "$BASE/health") check "GET /health" '"status":"ok"' "$R" -# 2. Ingest a payment webhook +# ── 2. Webhook Ingestion ───────────────────────────────────── echo "--- Webhook Ingestion ---" R=$(curl -s -X POST "$BASE/hooks/payment.completed" \ -H "Content-Type: application/json" \ @@ -40,13 +52,11 @@ R=$(curl -s -X POST "$BASE/hooks/payment.completed" \ check "POST /hooks/payment.completed" '"status":"accepted"' "$R" check " returns event ID" '"id":"test_1"' "$R" -# 3. Ingest a user webhook R=$(curl -s -X POST "$BASE/hooks/user.created" \ -H "Content-Type: application/json" \ -d '{"id":"test_2","name":"Alice"}') check "POST /hooks/user.created" '"status":"accepted"' "$R" -# 4. Ingest with auto-generated ID R=$(curl -s -X POST "$BASE/hooks/order.shipped" \ -H "Content-Type: application/json" \ -d '{"tracking":"TRACK123"}') @@ -55,32 +65,118 @@ check "POST /hooks/order.shipped (auto-ID)" '"status":"accepted"' "$R" # Give workers time to process sleep 1 -# 5. Query stats +# ── 3. Query API ───────────────────────────────────────────── echo "--- Query API ---" R=$(curl -s "$BASE/api/stats") check "GET /api/stats" '"total":3' "$R" check " all succeeded" '"ok":3' "$R" -# 6. Query specific event R=$(curl -s "$BASE/api/events/test_1") check "GET /api/events/test_1" '"type":"payment.completed"' "$R" check " has status" '"status":"ok"' "$R" -# 7. Query nonexistent event R=$(curl -s "$BASE/api/events/nonexistent") check "GET /api/events/nonexistent" '"error":"not found"' "$R" -# 8. Duplicate event (same ID) +# ── 4. Deduplication ───────────────────────────────────────── +echo "--- Deduplication ---" R=$(curl -s -X POST "$BASE/hooks/payment.completed" \ -H "Content-Type: application/json" \ -d '{"id":"test_1","amount":9999}') -check "POST duplicate ID accepted" '"status":"accepted"' "$R" +check "POST duplicate ID accepted (HTTP level)" '"status":"accepted"' "$R" + +sleep 1 + +# Stats should still show 3 processed (duplicate was dropped by pipeline) +R=$(curl -s "$BASE/api/stats") +check " dedup: total still 3" '"total":3' "$R" + +# ── 5. Crash Recovery ──────────────────────────────────────── +echo "--- Crash Recovery (supervisor restart) ---" + +# Send a crash event +R=$(curl -s -X POST "$BASE/hooks/test.crash" \ + -H "Content-Type: application/json" \ + -d '{"id":"crash_1","payload":"boom"}') +check "POST /hooks/test.crash accepted" '"status":"accepted"' "$R" + +sleep 2 # Give supervisor time to restart the worker + +# Verify the system is still operational after crash +R=$(curl -s -X POST "$BASE/hooks/payment.completed" \ + -H "Content-Type: application/json" \ + -d '{"id":"post_crash_1","amount":777}') +check " post-crash webhook works" '"status":"accepted"' "$R" -# 9. 404 for unknown route +sleep 1 + +R=$(curl -s "$BASE/api/events/post_crash_1") +check " post-crash event processed" '"status":"ok"' "$R" + +# ── 6. 404 ─────────────────────────────────────────────────── +echo "--- Error Handling ---" R=$(curl -s "$BASE/nope") check "GET /nope → 404" "Not Found" "$R" -# 10. Rapid-fire ingestion +# ── 7. HMAC Signature Verification ─────────────────────────── +echo "--- HMAC Signature Verification ---" +if [ "${EDGE_SECRET:-}" != "" ]; then + # Test: no signature → 401 + CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE/hooks/payment.completed" \ + -H "Content-Type: application/json" \ + -d '{"id":"hmac_nosig","amount":1}') + check_code " no signature → 401" "401" "$CODE" + + # Test: wrong signature → 401 + CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE/hooks/payment.completed" \ + -H "Content-Type: application/json" \ + -H "x-signature: deadbeef" \ + -d '{"id":"hmac_wrong","amount":1}') + check_code " wrong signature → 401" "401" "$CODE" + + # Test: valid signature → 202 + BODY='{"id":"hmac_valid","amount":1}' + SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$EDGE_SECRET" -hex 2>/dev/null | sed 's/.*= //') + CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE/hooks/payment.completed" \ + -H "Content-Type: application/json" \ + -H "x-signature: $SIG" \ + -d "$BODY") + check_code " valid signature → 202" "202" "$CODE" +else + echo " SKIP: HMAC tests (no EDGE_SECRET set — start with EDGE_SECRET=test-secret)" +fi + +# ── 8. WebSocket Dashboard ──────────────────────────────────── +echo "--- WebSocket Dashboard ---" +if command -v websocat &> /dev/null; then + # Connect WebSocket, capture initial stats message + WSOUT=$(timeout 3 websocat -1 "ws://localhost:${PORT}/dashboard" 2>/dev/null || true) + if [ -n "$WSOUT" ]; then + check " WebSocket initial stats" '"total"' "$WSOUT" + else + echo " SKIP: WebSocket connected but no data received" + fi + + # Connect and send an event to see live notification + timeout 4 websocat "ws://localhost:${PORT}/dashboard" > /tmp/ws-edge-test.out 2>/dev/null & + WS_PID=$! + sleep 1 + curl -s -X POST "$BASE/hooks/payment.completed" \ + -H "Content-Type: application/json" \ + -d '{"id":"ws_live","amount":42}' > /dev/null + sleep 2 + kill $WS_PID 2>/dev/null || true + wait $WS_PID 2>/dev/null || true + if [ -f /tmp/ws-edge-test.out ]; then + WSDATA=$(cat /tmp/ws-edge-test.out) + check " WebSocket live notification" '"event-processed"' "$WSDATA" + rm -f /tmp/ws-edge-test.out + fi +else + echo " SKIP: WebSocket tests (install websocat to enable)" +fi + +# ── 9. Load Test (50 events) ───────────────────────────────── echo "--- Load Test (50 events) ---" for i in $(seq 1 50); do curl -s --max-time 5 -X POST "$BASE/hooks/load.test" \ @@ -99,7 +195,7 @@ else FAIL=$((FAIL + 1)) fi -# Summary +# ── Summary ─────────────────────────────────────────────────── echo "" echo "=======================================" echo " Results: $PASS passed, $FAIL failed"