⬆️ bump simpleredis to the pooled build, teach the e2e mock RESP

The pooled simpleredis sends RESP arrays instead of inline commands, so
the e2e redis stand-in answered +OK to the "*2" header and the plugin
read "OK" as a cached verdict, blocking a request that should pass.
The mock now parses RESP arrays, still accepts inline, and handles MGET.

Points at pool-redis-connections until maxlerebourg/simpleredis#8 is
tagged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
mhx
2026-09-04 11:07:18 +02:00
co-authored by Claude Opus 5
parent 3ceb9617f2
commit 4e2bfeb142
6 changed files with 353 additions and 138 deletions
+65 -11
View File
@@ -15,10 +15,12 @@ import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"strconv"
"strings"
"sync"
)
@@ -49,10 +51,10 @@ func list(m map[string]Decision) []Decision {
return out
}
// --- Redis mock (inline-command wire format, as spoken by simpleredis) ---
// --- Redis mock (RESP arrays, as spoken by simpleredis; inline still accepted) ---
// serveRedis is a hardcoded stand-in. When verdicts is true it plays a replica
// that holds decisions: every line is scanned for known IPs, 1.2.3.4 → "f"
// that holds decisions: keys are scanned for known IPs, 1.2.3.4 → "f"
// (clean), 1.2.3.5 → "t" (banned); any other GET is a miss ($-1). When verdicts
// is false it plays the primary and answers every GET with a miss, so a
// scenario can prove reads are served from the replica and not the primary.
@@ -73,18 +75,25 @@ func serveRedis(addr string, verdicts bool) {
defer conn.Close()
rd := bufio.NewReader(conn)
for {
line, _, err := rd.ReadLine()
args, err := readRedisCommand(rd)
if err != nil {
return
}
s := string(line)
switch {
case verdicts && strings.Contains(s, "1.2.3.4"):
conn.Write([]byte("$1\r\nf\r\n"))
case verdicts && strings.Contains(s, "1.2.3.5"):
conn.Write([]byte("$1\r\nt\r\n"))
case strings.HasPrefix(strings.ToUpper(s), "GET "):
conn.Write([]byte("$-1\r\n"))
if len(args) == 0 {
continue
}
switch strings.ToUpper(args[0]) {
case "GET":
if len(args) < 2 {
conn.Write([]byte("$-1\r\n"))
continue
}
conn.Write([]byte(redisValue(verdicts, args[1])))
case "MGET":
fmt.Fprintf(conn, "*%d\r\n", len(args)-1)
for _, key := range args[1:] {
conn.Write([]byte(redisValue(verdicts, key)))
}
default:
conn.Write([]byte("+OK\r\n"))
}
@@ -93,6 +102,51 @@ func serveRedis(addr string, verdicts bool) {
}
}
func redisValue(verdicts bool, key string) string {
switch {
case verdicts && strings.Contains(key, "1.2.3.4"):
return "$1\r\nf\r\n"
case verdicts && strings.Contains(key, "1.2.3.5"):
return "$1\r\nt\r\n"
default:
return "$-1\r\n"
}
}
// readRedisCommand reads one RESP array, falling back to a whitespace split for
// the inline commands older simpleredis releases sent.
func readRedisCommand(rd *bufio.Reader) ([]string, error) {
header, err := rd.ReadString('\n')
if err != nil {
return nil, err
}
header = strings.TrimRight(header, "\r\n")
if !strings.HasPrefix(header, "*") {
return strings.Fields(header), nil
}
count, err := strconv.Atoi(header[1:])
if err != nil {
return nil, err
}
args := make([]string, 0, count)
for i := 0; i < count; i++ {
sizeLine, err := rd.ReadString('\n')
if err != nil {
return nil, err
}
size, err := strconv.Atoi(strings.TrimRight(sizeLine, "\r\n")[1:])
if err != nil {
return nil, err
}
buf := make([]byte, size+2)
if _, err := io.ReadFull(rd, buf); err != nil {
return nil, err
}
args = append(args, string(buf[:size]))
}
return args, nil
}
func main() {
lapiAddr := flag.String("lapi-addr", "127.0.0.1:8090", "address for the LAPI mock")
// The stub upstream Traefik proxies allowed requests to — the binary-suite