# Module 5 classroom examples

Use Docker Desktop or Docker Engine with Compose v2. All commands run in this
folder in a terminal. No host Python, Java, or Kafka installation is needed.
Use three terminal tabs. The examples use synthetic orders and print simulated
effects. The credentials are local demo values, not production credentials.

## Start from scratch

This lab is standalone. No earlier lab or project code is required. Download
`classroom-kit.zip` from Module 5, unzip it, and open a terminal in the extracted
`module-05` directory. Confirm it contains `compose.yaml`, `Dockerfile`,
`requirements.txt`, and `rabbit.py`. Install/start Docker with Compose v2, then
run the commands below. Repeat the same directory in each terminal tab.
Initial image downloads need internet and may take longer than the demo.

The agentic travel assignment is a separate implementation exercise: its brief
is linked from Module 5 and Assignments. This kit supplies brokers and small
messaging demos, not the complete agent application.

## Before class (allow time for image downloads)

```sh
docker compose version
docker compose pull rabbitmq kafka
docker compose build worker
docker compose up -d --wait rabbitmq kafka
```

Readiness must succeed before continuing. Kafka uses the official image's
single-node KRaft defaults, with no ZooKeeper. Kafka tools run inside its
container, so no host listener configuration is needed. These single brokers
are not a replication or high-availability demonstration. No persistent host
volumes are configured: removing containers resets the exercise data.

## A. RabbitMQ: share six pickup jobs (10 minutes)

Terminal 1:

```sh
docker compose run --rm worker python rabbit.py worker --name A
```

Terminal 2:

```sh
docker compose run --rm worker python rabbit.py worker --name B
```

Terminal 3:

```sh
docker compose run --rm worker python rabbit.py send --count 6
```

Expected: six CONFIRMED messages from the producer. Each ID goes to one worker
in this failure-free run. A and B share work; an exact 3/3 split is not promised.
Each worker prints RECEIVED, SIMULATED EFFECT, then ACK. A publisher confirm
means broker acceptance, not completion of pickup work.

Visit http://localhost:15672 and log in with classroom / classroom. Under Queues
and Streams, open pickup-jobs. Inspect Ready, Unacked, and Consumers. With two
workers and prefetch=1, at most two deliveries are unacknowledged at a time.
After completion Ready and Unacked should both reach zero.

Stop both workers with Ctrl+C. Publish three jobs. Ready should reach 3. Start
one worker again and watch the backlog drain. Broker acceptance lets the
producer finish while the workers are unavailable.

## B. RabbitMQ: repeat the same effect (10 minutes)

Stop every normal worker and let previous work finish first. Start this worker:

```sh
docker compose run --rm worker python rabbit.py worker --name CRASH --crash-before-ack
```

In another terminal publish exactly one job:

```sh
docker compose run --rm worker python rabbit.py send --count 1
```

The worker prints SIMULATED EFFECT then intentionally fails before ACK. Its
connection closes and RabbitMQ requeues the outstanding delivery. Start:

```sh
docker compose run --rm worker python rabbit.py worker --name RECOVERY
```

Expected: the same message ID appears with redelivered=True, another simulated
effect, then ACK. This is a predicted trace, not a captured test run:

```text
CRASH RECEIVED <same-id> o1 redelivered= False
CRASH SIMULATED EFFECT o1
RECOVERY RECEIVED <same-id> o1 redelivered= True
RECOVERY SIMULATED EFFECT o1
RECOVERY ACK <same-id>
```

Discuss: Where would an atomic processed-ID record and business update go?
Why would saving an ID separately from sending a real email be insufficient?
The program intentionally does not deduplicate, to expose the failure window.
Persistent messages, a durable queue, and confirms improve safety but this
single broker is still a failure domain. A poison message needs bounded retries
and a dead-letter path rather than an infinite requeue loop.

## C. Kafka: retain and replay three order events (15 minutes)

Create a topic with one partition so the first example has obvious ordering:

```sh
docker compose exec kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --if-not-exists --topic orders --partitions 1 --replication-factor 1
```

Terminal 1: start the pickup consumer and leave it running:

```sh
docker compose exec kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders --group pickup --from-beginning --property print.key=true --property print.partition=true --property print.offset=true
```

Terminal 2: open the interactive producer:

```sh
docker compose exec kafka /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic orders --property parse.key=true --property key.separator=:
```

Paste one record per line, then Ctrl+C to stop the producer:

```text
o42:{"id":"e1","type":"OrderConfirmed","schemaVersion":1,"orderId":"o42"}
o43:{"id":"e2","type":"OrderConfirmed","schemaVersion":1,"orderId":"o43"}
o42:{"id":"e3","type":"PickupReady","schemaVersion":1,"orderId":"o42"}
```

Expected: pickup prints all three events. On a fresh topic, partition 0 has
offsets 0, 1, 2. Re-running the producer appends more records; it does not replace
existing ones. The key precedes the first colon; the rest is the JSON value.

Terminal 3: start a separate application after publication:

```sh
docker compose exec kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders --group analytics --from-beginning --property print.key=true --property print.partition=true --property print.offset=true
```

Expected: analytics also reads all three retained records. Reading does not
remove them. Wait at least 10 seconds, then inspect committed progress:

```sh
docker compose exec kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group pickup
```

On the fresh three-record topic, CURRENT-OFFSET and LOG-END-OFFSET should become
3 and LAG 0. Offset 3 is the next position, not the last processed record.
Console consumers auto-commit periodically; these offsets do not demonstrate
an atomic business effect. Restarting the same group uses its committed
position. --from-beginning only chooses the earliest available position when
there is no valid committed position; it does not override an existing commit.

Stop analytics. Use a NEW group to replay retained history:

```sh
docker compose exec kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders --group audit-replay-1 --from-beginning --property print.key=true --property print.partition=true --property print.offset=true
```

For each further replay choose a new group name (audit-replay-2, etc.). Retention
bounds replay; a new group cannot recover data already removed by the broker.
Never replay real side effects without an explicit replay policy.

## D. Kafka: same group versus different groups (10 minutes)

Stop the analytics/replay consumer. Keep pickup running. In a second terminal
run the same pickup consumer command with --group pickup. Allow the rebalance
to finish, then produce another record using the producer command above:

```text
o44:{"id":"e4","type":"OrderConfirmed","schemaVersion":1,"orderId":"o44"}
```

Expected: with one partition, only one member of the pickup group gets the new
record in steady state. The other is idle. Stop the active member; the other
can take over after group membership changes. Failures and offset timing can
cause repeat reads. This is not an exactly-once demonstration.

Optional: create a NEW topic orders-parallel with --partitions 2 and repeat
using two consumers in a NEW group. Produce many distinct keys. Check group
assignment with --describe. Both partitions can be assigned, but two different
keys may hash to the same partition; an even traffic split is not guaranteed.
Ordering is within a partition, not across the topic. Changing the partition
count can change the mapping of future records for a key.

## Stop, reset, and troubleshoot

Ctrl+C all foreground clients, then:

```sh
docker compose down
```

This removes the demo containers and their writable data. The next startup is
a clean exercise. Do not use this reset on data you need to keep.

- Broker unhealthy: inspect `docker compose logs rabbitmq kafka`; allow more
  Docker memory or startup time and rerun `docker compose up -d --wait`.
- Port 15672 occupied: change only the host side to 15673 and use that UI port.
- RabbitMQ queue precondition error: old queue declarations may differ. Reset
  this disposable exercise or use a fresh queue name consistently in the code.
- No RabbitMQ redelivery: stop every other worker before the crash experiment.
- Kafka receives nothing: check topic, group, and whether offsets already exist.
  Use a fresh group for replay and publish after consumers are assigned.
- Cannot download images: use the paper activity in the module. Prepare images
  before class; the first run requires internet access.

## Rehearsal checklist

These are acceptance checks for a Docker-enabled instructor machine:

- Both broker health checks pass.
- Six distinct RabbitMQ IDs are acknowledged, with an empty queue afterward.
- Crash/recovery shows the SAME ID and redelivered=True.
- Two different Kafka groups each see the three retained events.
- A fresh replay group sees history; two members of pickup share one partition.
- Reset and repeat once before class.

References: [RabbitMQ work queues](https://www.rabbitmq.com/tutorials/tutorial-two-python),
[confirms and acknowledgements](https://www.rabbitmq.com/docs/confirms),
[Kafka quickstart](https://kafka.apache.org/41/getting-started/quickstart/).
