#!/usr/bin/env bash
# Real demos for P057 "How Redis Caching Works". Runs in Docker on render01. Ana's tea shop lab, on one Docker
# network with no way out (--internal): "db" (the official postgres:18 image, 20 teas and 4 million past orders),
# "cache" (the official redis:8-alpine image, started with a 20 MB memory limit, no snapshots) and "laptop" (Python
# 3.13 with redis-py, psycopg and redis-cli, user ana, lab/shop.py). Every command is typed into one real
# interactive bash on the laptop (a pty); demo_output.json keeps every line, and build_script.py reads every number
# from it. Takes ~8 min: one real 5-minute TTL runs out on the way.
set -euo pipefail
cd "$(dirname "$0")"
P=ce-p057
cleanup() { docker rm -f $P-laptop $P-db $P-cache >/dev/null 2>&1 || true; docker network rm $P >/dev/null 2>&1 || true; }
cleanup
trap cleanup EXIT
docker build -q -t $P-laptop lab >/dev/null
docker network create --internal $P >/dev/null
docker run -d --name $P-db --hostname db --network $P --network-alias db -e POSTGRES_USER=shop \
  -e POSTGRES_PASSWORD=shop -e POSTGRES_DB=shop postgres:18 >/dev/null
docker run -d --name $P-cache --hostname cache --network $P --network-alias cache redis:8-alpine \
  redis-server --maxmemory 20mb --maxmemory-policy noeviction --save "" --appendonly no >/dev/null
until docker exec $P-db pg_isready -U shop -d shop -q; do sleep 1; done
sleep 3
until docker exec $P-db pg_isready -U shop -d shop -q; do sleep 1; done
docker exec -i $P-db psql -q -U shop -d shop < lab/schema.sql
docker run -d --name $P-laptop --hostname laptop --network $P -e TERM=dumb $P-laptop sleep infinity >/dev/null
docker cp lab/shop.py $P-laptop:/home/ana/shop/shop.py
# off screen: warm PostgreSQL's own memory, so the slow page is slow every time, not just the first
for i in 1 2; do docker exec $P-laptop python3 shop.py bestsellers --no-cache >/dev/null; done
VERS=$(docker exec $P-cache redis-server --version | grep -o 'v=[0-9.]*'; docker exec $P-db postgres --version)

python3 - "$P" "$VERS" <<'PY'
import fcntl, json, os, pty, re, select, struct, subprocess, sys, termios, time
P, VERS = sys.argv[1], sys.argv[2]
READY = re.compile(rb"__READY__:(.*?):__ ")
steps = {}
class Shell:
    def __init__(self):
        pid, fd = pty.fork()
        if pid == 0:
            fcntl.ioctl(0, termios.TIOCSWINSZ, struct.pack("HHHH", 50, 100, 0, 0))
            os.execvp("docker", ["docker", "exec", "-it", "-e", "TERM=dumb", f"{P}-laptop", "bash", "-i"])
        self.fd, self.where = fd, "~/shop"
        os.write(fd, b"stty -echo; stty cols 100 rows 50; PS1='__REA''DY__:\\w:__ '\n")
        self.read()
    def read(self, timeout=600):
        buf, end = b"", time.time() + timeout
        while time.time() < end:
            if select.select([self.fd], [], [], 0.3)[0]:
                buf += os.read(self.fd, 65536)
                m = None
                for m in READY.finditer(buf):
                    pass
                if m:
                    self.where = m.group(1).decode()
                    out = buf[:m.start()].replace(b"\r\r\n", b"\n").replace(b"\r\n", b"\n").decode()
                    return "\n".join(l.rstrip() for l in out.split("\n")).strip("\n")
        raise SystemExit(f"timed out; last output: {buf[-600:]!r}")
    def run(self, key, cmd):
        shown = f"ana@laptop:{self.where}$ {cmd}"
        os.write(self.fd, cmd.encode() + b"\n")
        out = self.read()
        steps[key] = {"cmd": shown, "out": out, "at": round(time.time(), 1)}
        print(f"--- {key}\n{shown}\n{out}\n", flush=True)
        time.sleep(0.5)
s = Shell()
# the slow page, with no cache
s.run("slow1", "python3 shop.py bestsellers --no-cache")
s.run("slow2", "python3 shop.py bestsellers --no-cache")
# what Redis is
s.run("ping", "redis-cli -h cache PING")
s.run("set", "redis-cli -h cache SET greeting hello")
s.run("get", "redis-cli -h cache GET greeting")
s.run("get_none", "redis-cli -h cache GET nothing-here")
# cache-aside
s.run("miss", "python3 shop.py bestsellers")
s.run("hit", "python3 shop.py bestsellers")
s.run("hit2", "python3 shop.py bestsellers")
s.run("value", "redis-cli -h cache GET shop:bestsellers")
s.run("ttl", "redis-cli -h cache TTL shop:bestsellers")
# let the real TTL run out (off screen)
t0 = steps["miss"]["at"]
while time.time() < t0 + 305:
    time.sleep(2)
s.run("ttl_gone", "redis-cli -h cache TTL shop:bestsellers")
s.run("miss_again", "python3 shop.py bestsellers")
# stale data
s.run("price1", "python3 shop.py price 3")
s.run("price2", "python3 shop.py price 3")
s.run("setprice", "python3 shop.py set-price 3 11.50")
s.run("stale", "python3 shop.py price 3")
s.run("stale_ttl", "redis-cli -h cache TTL shop:product:3")
s.run("setprice_fix", "python3 shop.py set-price 3 11.50 --clear-cache")
s.run("fresh", "python3 shop.py price 3")
# memory is limited
s.run("maxmem", "redis-cli -h cache CONFIG GET maxmemory")
s.run("policy", "redis-cli -h cache CONFIG GET maxmemory-policy")
s.run("fill_oom", "python3 shop.py fill 40000")
s.run("used", "redis-cli -h cache INFO memory | grep used_memory_human")
s.run("lru", "redis-cli -h cache CONFIG SET maxmemory-policy allkeys-lru")
s.run("fill_ok", "python3 shop.py fill 40000")
s.run("evicted", "redis-cli -h cache INFO stats | grep evicted_keys")
s.run("dbsize", "redis-cli -h cache DBSIZE")
s.run("first_gone", "redis-cli -h cache EXISTS shop:page:1")
s.run("last_kept", "redis-cli -h cache EXISTS shop:page:40000")
# a stampede
s.run("rush", "python3 shop.py rush 50")
s.run("rush_lock", "python3 shop.py rush 50 --lock")
# a cache is a copy
s.run("flush", "redis-cli -h cache FLUSHALL")
s.run("after_flush", "python3 shop.py bestsellers")
for k in steps:
    steps[k].pop("at", None)
steps["_meta"] = {"cmd": "", "out": json.dumps({"run_at": time.strftime("%Y-%m-%d %H:%M %Z"), "versions": VERS})}
json.dump(steps, open("demo_output.json", "w"), indent=1)
print("saved demo_output.json")
PY
