DEV Community

Charlie Zhu
Charlie Zhu

Posted on

The Budget Card Workshop

A lab room at 3:40 p.m. still had one process running. The previous session had asked a coding assistant for a robust HTTP client with retries, then pointed that client at a tiny charging endpoint that failed on purpose. The assistant produced a while loop. The loop treated every timeout as a reason to try again. No ceiling. No receipt. Students packed laptops while the terminal kept printing the same line, like a metronome that refused to leave the stage.

That scene is the whole workshop. AI-generated clients often sound careful. They mention backoff. They mention transient errors. They still forget an old machine-room rule: every outbound call spends a card, and when the card is empty the machine stops.

This outline lasts two hours. It is for people who review patches they did not fully type. It is not a product tour. The artifact is a budget card: a small Python object that travels with each request, counts hops, preserves one request stamp, and fails closed. Students can rerun the worked example on a laptop with only the standard library.

The first ten minutes belong to the upstream. Students save flaky_upstream.py and start it on port 8765. The server is intentionally rude. The first two POST requests to /charge for a given stamp return 503. The third returns 200 and echoes the stamp. That pattern is the classroom weather: enough failure to trigger retries, enough success to prove a ceiling worked.

#!/usr/bin/env python3
"""Flaky charging endpoint for the Budget Card Workshop."""
from collections import defaultdict
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json

FAILS_BEFORE_OK = 2
HITS = defaultdict(int)

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        if self.path != "/charge":
            self.send_error(404)
            return
        stamp = self.headers.get("X-Request-Stamp", "")
        length = int(self.headers.get("Content-Length", "0"))
        body = self.rfile.read(length)
        HITS[stamp] += 1
        hop = HITS[stamp]
        payload = {"ok": hop > FAILS_BEFORE_OK, "hop": hop, "stamp": stamp}
        raw = json.dumps(payload).encode("utf-8")
        status = 200 if payload["ok"] else 503
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(raw)))
        self.send_header("X-Echo-Stamp", stamp)
        self.end_headers()
        self.wfile.write(raw)

    def log_message(self, fmt, *args):
        print("upstream", args[0])

if __name__ == "__main__":
    server = ThreadingHTTPServer(("127.0.0.1", 8765), Handler)
    print("flaky upstream on 127.0.0.1:8765")
    server.serve_forever()
Enter fullscreen mode Exit fullscreen mode

Start it in its own terminal. The room should see one line that names the port.

python3 flaky_upstream.py
Enter fullscreen mode Exit fullscreen mode

If the bind fails, another process still owns 8765. Change the port in the server and in the client together. Do not wrestle the operating system for a number.

The next twenty minutes are for capturing a naive client. Students may type it by hand. They may also ask a coding assistant for a robust retrying client, which is how the runaway loop usually arrives. MonkeyCode can host that generation step if the classroom already uses it. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The project offers free model access and a free server option, which is enough for this lab when a second upstream is useful later. No model name is required. The lesson is the shape of the loop, not the label on the prompt box.

A typical naive client looks calm. It is not.

# naive_client.py — proposed classroom foil, not production code
import json, time, urllib.error, urllib.request

URL = "http://127.0.0.1:8765/charge"

def charge(payload):
    body = json.dumps(payload).encode("utf-8")
    attempt = 0
    while True:
        attempt += 1
        req = urllib.request.Request(URL, data=body, method="POST")
        req.add_header("Content-Type", "application/json")
        try:
            with urllib.request.urlopen(req, timeout=2) as resp:
                print("ok", attempt, resp.read().decode())
                return
        except urllib.error.HTTPError as exc:
            print("retry", attempt, exc.code)
            time.sleep(0.2)

if __name__ == "__main__":
    charge({"sku": "lab-seat", "cents": 1})
Enter fullscreen mode Exit fullscreen mode

Run it against the flaky server. Three attempts can look like design. Change FAILS_BEFORE_OK to 6 and the same client will sit there, polite and endless. The analogy is a subway turnstile that never counts passengers. The gate keeps swinging.

Minute thirty begins the budget card. Students add a tiny dataclass. The card holds a hop ceiling, a hop counter, and a request stamp created once. The client must refuse to send when no hops remain. That refusal is the lesson. Retries are not virtue. They are a spent resource.

# budget_card.py — worked example students can rerun
from __future__ import annotations

from dataclasses import dataclass, field
import json, time, uuid, urllib.error, urllib.request

URL = "http://127.0.0.1:8765/charge"

@dataclass
class BudgetCard:
    max_hops: int
    hops_used: int = 0
    stamp: str = field(default_factory=lambda: uuid.uuid4().hex)

    def remaining(self) -> int:
        return self.max_hops - self.hops_used

    def punch(self) -> None:
        if self.remaining() <= 0:
            raise RuntimeError(f"CEILING stamp={self.stamp} hops={self.hops_used}")
        self.hops_used += 1

def charge(payload: dict, card: BudgetCard) -> dict:
    body = json.dumps(payload).encode("utf-8")
    last_error = None
    while True:
        try:
            card.punch()
        except RuntimeError as exc:
            print(f"receipt stamp={card.stamp} hops={card.hops_used} status=CEILING")
            raise
        req = urllib.request.Request(URL, data=body, method="POST")
        req.add_header("Content-Type", "application/json")
        req.add_header("X-Request-Stamp", card.stamp)
        try:
            with urllib.request.urlopen(req, timeout=2) as resp:
                echo = resp.headers.get("X-Echo-Stamp", "")
                raw = resp.read().decode("utf-8")
                if echo != card.stamp:
                    raise RuntimeError(f"stamp drift sent={card.stamp} echo={echo}")
                print(f"receipt stamp={card.stamp} hops={card.hops_used} status={resp.status}")
                return json.loads(raw)
        except urllib.error.HTTPError as exc:
            last_error = exc
            print(f"hop {card.hops_used} http {exc.code}")
            time.sleep(0.2)
        except urllib.error.URLError as exc:
            last_error = exc
            print(f"hop {card.hops_used} url {exc.reason}")
            time.sleep(0.2)
    raise RuntimeError(f"exhausted without receipt: {last_error}")

if __name__ == "__main__":
    tight = BudgetCard(max_hops=2)
    try:
        charge({"sku": "lab-seat", "cents": 1}, tight)
    except RuntimeError as exc:
        print("expected tight failure:", exc)
    loose = BudgetCard(max_hops=3)
    print(charge({"sku": "lab-seat", "cents": 1}, loose))
Enter fullscreen mode Exit fullscreen mode

The instructor should force a mismatch on purpose. Leave the server at two failures before success. Set max_hops to 2 first. Students watch a clean CEILING instead of a hang. Then they raise the ceiling to 3 and watch the echo come back. The two runs are the same script. Only the card changes.

python3 budget_card.py
Enter fullscreen mode Exit fullscreen mode

After a short break, twenty minutes go to the sticky stamp. Assistant patches often mint a new UUID on every retry, which turns a retry into a new order. The budget card creates the stamp once and copies it onto X-Request-Stamp for every hop. The server echoes it. Students compare the echoed value to the card. If the two strings differ, the client is lying about identity, even if the HTTP status looks fine.

A quick probe makes that lie visible. Students can temporarily move uuid.uuid4().hex inside the while loop, rerun, and read stamp drift on the third hop. Then they move the stamp back onto the dataclass. The failure is the teaching object. The fix is one field that does not get reborn.

The next block is fail-closed logging. The client already prints a one-line receipt: stamp, hops used, and either a status code or the word CEILING. Keep that line boring. Reviewers can grep it. A dashboard can wait. A classroom does not need a metrics vendor to learn that unbounded retry is just a loop with better copy.

The last twenty minutes are optional and remote. A local flaky server teaches the mechanism. A hosted upstream teaches the other failure: extra latency, a cold process, and the temptation to raise the ceiling just this once. Classrooms that already have MonkeyCode’s free server option can point URL at that base path instead of localhost, still sending only the synthetic lab-seat payload. Do not send customer records. Do not send secrets. A budget card is not an access-control system, and a free shared host is not a vault.

Limitations are part of the lab, not a footnote. The server counts hits in process memory, so a restart wipes the flaky weather. The client uses urllib on purpose, which means no connection pool and no HTTP/2 puzzles. Backoff is a fixed 200 milliseconds, which is a metronome, not a production jitter policy. The receipt is stdout. It will not survive a crashed terminal. None of that is accidental. The workshop isolates one idea: hops are finite, and identity must not reset when a hop is spent.

This approach is a poor fit for teams that need load tests, circuit breakers across many hosts, or idempotency stored in a real ledger. It is also the wrong tool for anyone pointing a classroom script at a live billing API. Students who cannot keep synthetic payloads synthetic should stop at the local server. Instructors who cannot read the receipt line in a code review should not add more retries to hide that fact.

The closing exercise is a one-file replay. Students delete __pycache__, restart flaky_upstream.py, and run budget_card.py twice. The tight card must print CEILING. The loose card must print status=200 with the same stamp the server echoed. If those two sentences do not hold, the room has not finished. If they hold, the assistant’s original while True can be thrown away without ceremony.

Readers who want a second upstream after the local run can point the same budget card at MonkeyCode’s free server option and leave the punch logic untouched. The card does not care where the 503 came from. It only cares that the next hop is still allowed.

Top comments (0)