Olá! 🚀 Recebi o enunciado completo da Atividade ...

Criado em: 22 de maio de 2025

Respondido usando O3 por Chat01

Pergunta

aula8-Docker.pdf
aula12-API.pdf
aula9-Cloud.pdf
aula10-Redis.pdf
aula11-Nginx.pdf
Atividade-02-Enunciado.pdf

Olá! 🚀

Recebi o enunciado completo da Atividade 2 – Projeto de um sistema key-value distribuído. Há bastantes partes envolvidas – da escolha da arquitetura e protocolos de consistência, até ao Docker + start.sh, testes de carga e documentação Atividade-02-Enunciado.

Para lhe ser mais útil, diga-me em que ponto está agora e o que precisa primeiro:

Desenho de alto nível
Escolher modelo de partida (p.ex. hash anel com N réplicas + coordenação Raft VS arquiteturas baseadas em Dynamo/Consistent-Hashing).

Exemplos de micro-serviços
Skeleton em Go/Python/Node com REST (FastAPI, Express, Gin…), storage local, health-checks, testes.

Orquestração Docker-Compose/K8s
Ficheiros docker-compose.yml, service discovery, auto-scaling e script start.sh.

Plano de testes de carga e relatório
Ferramentas (Locust, k6), métricas e como justificar limites.

Swagger/OpenAPI + README template
Gerar documentação e exemplos de chamadas (GET/PUT/DELETE).

Qualidade de sistema distribuído
Como documentar concorrência, escalabilidade, tolerância a falhas, consistência.

Se preferir, posso:

montar um road-map detalhado com milestones até 23 de maio;

gerar um esqueleto completo do repo (código + Docker + documentação inicial);

focar-me num ponto específico (p.ex. algoritmo de consenso, persistência, testes).

Distributed Key‑Value Store – Atividade 2

Stack: Python 3.12 · FastAPI · Redis (cache) · RabbitMQ (queue) · SQLite/CockroachDB (storage) · Nginx (LB) · Docker Compose

1. Subir o cluster localmente

bash
git clone <repo> cd distributed-kv ./start.sh # ⚡ pull, build e `docker compose up -d` ## 2. Exemplos Curl curl -X PUT -H "Content-Type: application/json" \ -d '{{"value":"hello"}}' http://localhost/kv/foo curl http://localhost/kv/foo # {{"data":{{"value":"hello"}}}} curl -X DELETE http://localhost/kv/foo 7. Deploy em Cloud (esboço) 3 VMs (API), 3 VMs (Consumers) atrás de um ALB/NLB. Managed CockroachDB ou k8s StatefulSet + Headless Service + HAProxy. ElastiCache Redis cluster (3 shards) Amazon MQ (RabbitMQ) em cluster. Autoscaling Groups p/ APIs e Consumers Ser resiliente na base de dados, fazer cluster na base de dados 2 filas put e delete Cluster Rabbitmq, cluster redis (se possível), cluster Database. Vários APIs, vários consumers, vários load balancer (se possível) HProx a frente do cluster da base de dados Fazer update na cache caso a Key que esteja a ser inserida na base de dados já exista no cache Usar timestamp para verificar se o value para atualizar na bd é mais antigo que o que já lá existe Testes de carga - Jmeter, siege, ab Tempo por request - Máximo de 1 segundo Tem que fazer pelo menos 5 tentativas antes de dar o rabbitmq como erro (fazer isto com base de dados e isso) Documentar como funcionaria a aplicação em cloud (prof não vai testar) Aqui segue a estrutura: Client -> Load Balancer -> API -> In memory Cache <- Consumer |-> RabbitMQ (varios) <---| |-> database <---| QUERO A IMPLEMENTACAO COMPLETA E PASSO A PASSO PODES FAZER PASTA POR PASTA. UMA DE CADA VEZ

Pensamento

Clarifying next steps

We need to ask the user where they'd like to begin, offering initial options based on what they need. They suggested a roadmap or focusing on specific points, so we should offer a starting point, like creating the repo skeleton. To plan this, we could lay out step-by-step details, allowing the user to confirm each step. Also, we'll provide code snippets inline and avoid any tools that are currently disabled, like canvas. No browsing or file search seems required here.

Thought for 56 seconds

Resposta

🚧 Projeto “Distributed-KV” – vamos passo a passo 🚧

A atividade pede que o repositório contenha código, Docker, start.sh, documentação, testes e relatório de carga . Para facilitar, proponho dividirmos o trabalho em pastas-marco (milestones). Cada etapa gera uma sub-árvore do repo já funcional, para que possa testar antes de avançar.


0 ▸ Road-map até 23 de maio

SemanaEntregávelConteúdo-chave
hojeMilestone 1 – SkeletonEstrutura de diretórios, docker-compose.yml mínimo, start.sh, README rascunho
+2 dMilestone 2 – API & ConsumerFastAPI (Python 3.12) + health-checks, RabbitMQ publish/consume, SQLite fallback
+4 dMilestone 3 – Cache & LBRedis cluster + Nginx LB (round-robin, timeouts)
+5 dMilestone 4 – EscalabilidadeVariáveis de escala (--scale api=3 consumer=3), readiness probes, autoscale notes
+6 dMilestone 5 – Testes de cargaScripts k6/Locust, target ≤ 1 s por request, retries=5
+7 dMilestone 6 – Docs finaisSwagger/OpenAPI, relatório, diagrama arquitetura, guia cloud-deploy

(Se precisar encurtar, juntamos 3 + 4 ou 5 + 6.)


1 ▸ Milestone 1 – Skeleton do repositório

text
distributed-kv/ ├── api/ # FastAPI service │ ├── main.py │ ├── models.py │ └── requirements.txt ├── consumer/ # Worker que processa filas │ └── worker.py ├── infra/ # Docker, Nginx, Redis, RabbitMQ, CockroachDB │ ├── docker-compose.yml │ └── nginx/ │ └── nginx.conf ├── scripts/ │ └── start.sh ├── tests/ │ └── load/ # k6 ou Locust └── docs/ ├── architecture.md ├── api.md # Swagger gera aqui └── README.md

a) start.sh (bootstrap único exigido)

bash
#!/usr/bin/env bash set -e echo "⏳ Build & up containers..." docker compose -f infra/docker-compose.yml pull docker compose -f infra/docker-compose.yml up -d --build echo "✅ Cluster running – veja http://localhost/docs para Swagger."

b) infra/docker-compose.yml – esqueleto mínimo

O slide “Exemplo – API para key-value (Ficheiro compose)” (p. 13, Aula 12) já mostra um template . A versão inicial reduzida:

yaml
version: "3.8" services: api: build: ../api ports: ["8000:8000"] depends_on: [redis, rabbitmq, db] environment: REDIS_HOST: redis AMQP_HOST: rabbitmq DB_HOST: db consumer: build: ../consumer depends_on: [rabbitmq, db] environment: AMQP_HOST: rabbitmq DB_HOST: db redis: image: redis:7.2-alpine ports: ["6379:6379"] rabbitmq: image: rabbitmq:3-management ports: ["15672:15672", "5672:5672"] db: image: cockroachdb/cockroach:v24.1.0 command: start-single-node --insecure ports: ["26257:26257"] networks: { default: {} }

*(Redis e RabbitMQ em cluster virão nos próximos marcos – ver slides Aula 10 & 11) *


2 ▸ Como vamos trabalhar “pasta por pasta”

  1. Você confirma: “Ok, cria a pasta X”.
  2. Eu envio o conteúdo completo dessa pasta (código ou configs) + explicação de como testar.
  3. Você roda localmente (./scripts/start.sh, docker compose logs etc.) e dá feedback.
  4. Seguimos para a próxima.

Isso garante que nunca entrego um dump gigante e você controla o ritmo.


Próximo passo

Quer começar gerando a pasta api/ com o FastAPI básico e endpoint /kv (GET/PUT/DELETE)?
Ou prefere primeiro fechar o docker-compose.yml e start.sh?

Diga qual pasta atacamos primeiro e já lhe envio o conteúdo completo. 😉

Pergunta

Ja criei essas 2

Proximas pastas

Pensamento

Clarifying next steps

The user mentioned they already created two folders from earlier suggestions ("api/" and perhaps "consumer/"), so now it's about moving forward with the next step. It seems the next folders should be 'api/' and possibly 'infra/'. But let me clarify this further to avoid confusion. Should I send the next content for 'api/' or 'infra/'? I'll continue the process by providing the information for the next folder based on the initial conversation.

Clarifying folder creation process

Alright, the user appears to have created the 'api/' and 'infra/' folders, as indicated by their response "Ja criei essas 2." Since they asked for one folder at a time, I’ll start with the 'consumer/' folder. This will include the Dockerfile, requirements.txt, and the Python worker.py file. The code will connect to RabbitMQ, CockroachDB, and Redis, including retry logic. I'll also update the docker-compose.yml accordingly and will take care to cite the relevant sources for the code implementation.

Thought for 58 seconds

Resposta

Pasta consumer/ – Worker que processa as filas RabbitMQ 🛠️

Siga estes três ficheiros (coloque-os dentro de distributed-kv/consumer/):


1. Dockerfile

dockerfile
FROM python:3.12-slim WORKDIR /app # dependências COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # código COPY worker.py . CMD ["python", "worker.py"]

2. requirements.txt

text
pika==1.3.2 # RabbitMQ (AMQP) psycopg[binary]==3.1.18 # CockroachDB/Postgres redis==5.0.4 # cache python-dotenv==1.0.1 # ler variáveis locais, se quiser

3. worker.py

python
import json, os, time, logging import pika, psycopg, redis from psycopg.rows import dict_row logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") # ── 1. Parâmetros de ambiente ────────────────────────────────────────────────── AMQP_HOST = os.getenv("AMQP_HOST" , "rabbitmq") DB_HOST = os.getenv("DB_HOST" , "db") DB_PORT = int(os.getenv("DB_PORT" , 26257)) DB_USER = os.getenv("DB_USER" , "root") DB_PASS = os.getenv("DB_PASS" , "") DB_NAME = os.getenv("DB_NAME" , "kv") REDIS_HOST = os.getenv("REDIS_HOST", "redis") REDIS_PORT = int(os.getenv("REDIS_PORT", 6379)) # ── 2. Conexões ──────────────────────────────────────────────────────────────── def conn_rmq(): while True: try: params = pika.ConnectionParameters(host=AMQP_HOST) return pika.BlockingConnection(params) except Exception as e: logging.warning("RabbitMQ indisponível, retry em 3 s: %s", e) time.sleep(3) def conn_db(): dsn = f"postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}" while True: try: return psycopg.connect(dsn, autocommit=True, row_factory=dict_row) except Exception as e: logging.warning("DB indisponível, retry em 3 s: %s", e) time.sleep(3) def ensure_schema(cur): cur.execute(""" CREATE TABLE IF NOT EXISTS kv_store ( key TEXT PRIMARY KEY, value TEXT, last_updated TIMESTAMPTZ DEFAULT now() ) """) rmq = conn_rmq() channel = rmq.channel() channel.queue_declare("add_key") channel.queue_declare("del_key") db = conn_db() cur = db.cursor() ensure_schema(cur) rcache = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True) # ── 3. Handlers ──────────────────────────────────────────────────────────────── MAX_RETRIES = 5 def handle_add(ch, method, properties, body): try: payload = json.loads(body) k, v, ts = payload["key_name"], payload["key_value"], payload["timestamp"] cur.execute(""" INSERT INTO kv_store (key, value, last_updated) VALUES (%s,%s,%s) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, last_updated = EXCLUDED.last_updated WHERE kv_store.last_updated <= EXCLUDED.last_updated """, (k, v, ts)) # cache “write through” rcache.set(k, v) logging.info("[add_key] %s=%s", k, v) ch.basic_ack(method.delivery_tag) except Exception as e: logging.error("[add_key] erro: %s", e) ch.basic_nack(method.delivery_tag, requeue=False) def handle_del(ch, method, properties, body): try: payload = json.loads(body) k, ts = payload["key_name"], payload["timestamp"] # verifica se há versão mais recente cur.execute("SELECT last_updated FROM kv_store WHERE key=%s", (k,)) row = cur.fetchone() if row and row["last_updated"] <= ts: cur.execute("DELETE FROM kv_store WHERE key=%s", (k,)) rcache.delete(k) logging.info("[del_key] %s removida", k) ch.basic_ack(method.delivery_tag) except Exception as e: logging.error("[del_key] erro: %s", e) ch.basic_nack(method.delivery_tag, requeue=False) channel.basic_consume("add_key", handle_add) channel.basic_consume("del_key", handle_del) logging.info("🟢 Consumer a escutar filas…") channel.start_consuming()

O fluxo baseia-se no snippet de consumidor apresentado nos slides da Aula 12, pág. 15, adaptado para Python e CockroachDB/Redis.


Como testar

  1. Suba o cluster (na raiz do projeto):

    bash
    ./scripts/start.sh # já existente docker compose logs -f consumer
  2. Publique uma chave:

    bash
    curl -X PUT -H "Content-Type: application/json" \ -d '{"data":{"key":"foo","value":"bar"}}' http://localhost:8000/kv
  3. Verifique no log do consumer que a mensagem add_key foi processada e o valor guardado em DB + Redis.

  4. Apague:

    bash
    curl -X DELETE "http://localhost:8000/kv?key=foo"

    O consumer processará del_key e retirará a entrada.


Próximo passo

Quando confirmar que o consumer está a funcionar, podemos avançar para a pasta tests/ (scripts de carga k6/Locust) ou para cluster Redis + Nginx – você escolhe. 😉

Pergunta

justm@Multitasking MINGW64 ~/OneDrive/Ambiente de Trabalho/SPD/Projeto Zuolo/Project_Key_Value/distributed-kv
$ ./scripts/start.sh # já existente
⏳ Build & up containers...
api Skipped - No image to be pulled
consumer Skipped - No image to be pulled
db Pulling
rabbitmq Pulling
redis Pulling
db Pulled
rabbitmq Pulled
redis Pulled
Compose can now delegate builds to bake for better performance.
To do so, set COMPOSE_BAKE=true.
#0 building with "desktop-linux" instance using docker driver

#1 [consumer internal] load build definition from Dockerfile
#1 transferring dockerfile: 234B done
#1 DONE 0.0s

#2 [api internal] load build definition from Dockerfile
#2 transferring dockerfile: 2B done
#2 DONE 0.0s

#3 [consumer internal] load metadata for docker.io/library/python:3.12-slim
#3 CANCELED
failed to solve: failed to read dockerfile: open Dockerfile: no such file or directory

Pergunta

justm@Multitasking MINGW64 ~/OneDrive/Ambiente de Trabalho/SPD/Projeto Zuolo/Project_Key_Value/distributed-kv
$ ./scripts/start.sh # já existente
⏳ Build & up containers...
api Skipped - No image to be pulled
consumer Skipped - No image to be pulled
db Pulling
rabbitmq Pulling
redis Pulling
db Pulled
rabbitmq Pulled
redis Pulled
Compose can now delegate builds to bake for better performance.
To do so, set COMPOSE_BAKE=true.
#0 building with "desktop-linux" instance using docker driver

#1 [consumer internal] load build definition from Dockerfile
#1 transferring dockerfile: 234B done
#1 DONE 0.0s

#2 [api internal] load build definition from Dockerfile
#2 transferring dockerfile: 2B done
#2 DONE 0.0s

#3 [consumer internal] load metadata for docker.io/library/python:3.12-slim
#3 CANCELED
failed to solve: failed to read dockerfile: open Dockerfile: no such file or directory

Pensamento

Diagnosing Docker error

The user ran the start script and encountered an error stating they couldn't find the Dockerfile. The logs suggest the "consumer" and possibly the "api" service directories are missing the Dockerfile. The user had previously created Dockerfiles but may have a path mismatch in the docker-compose.yml file. It references build: ../consumer, which should be correct if the consumer folder exists. However, the error indicates the files are missing, so the issue might be that the user hasn’t created the Dockerfiles in those directories yet.

Helping with Dockerfile error

The error suggests the consumer directory exists, but the Dockerfile is missing. The user may have pasted the Dockerfile content but forgotten to save it correctly. We need to help fix the build failure by ensuring the Dockerfile is present in both the api/ and consumer/ directories. We should check the folder structure and clarify that the docker-compose.yml path for the build context must be accurate. Finally, we should guide them to create a complete Dockerfile and update the start.sh script with the necessary build commands.

Thought for 42 seconds

Resposta

Por quê o docker compose falhou

O erro

failed to read dockerfile: open Dockerfile: no such file or directory

significa que, dentro do context indicado no docker-compose.yml, o Docker não encontra um ficheiro chamado Dockerfile.
No seu caso está a acontecer para consumer/ (e possivelmente para api/, que parece ter um Dockerfile vazio - “2 B done”).

No exemplo-guia da Aula 12 o docker-compose.yml aponta para ./api, assumindo que o ficheiro está na mesma pasta do compose .

Como na nossa estrutura o compose vive em infra/, usamos ../consumer e ../api:

text
infra/docker-compose.yml build: ../consumer # caminho relativo à pasta infra

Se o ficheiro não existir (ou está vazio) o build falha.


Check-list rápida para corrigir

  1. Confirme os caminhos

    bash
    tree -L 2 # deve mostrar: # distributed-kv/ # ├── api/ # │ └── Dockerfile # ├── consumer/ # │ └── Dockerfile # └── infra/ # └── docker-compose.yml
  2. Crie/preencha o Dockerfile em cada serviço

    api/Dockerfile (min version – FastAPI + Uvicorn):

    dockerfile
    FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

    consumer/Dockerfile (o que lhe enviei):

    dockerfile
    FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY worker.py . CMD ["python", "worker.py"]
  3. Garanta que o nome do ficheiro é exactamente Dockerfile
    (sem extensão, sem maiúsculas diferentes).

  4. Volte a executar

    bash
    ./scripts/start.sh # isto corre ‘docker compose up -d --build’ docker compose -f infra/docker-compose.yml ps

    Agora deverá ver api, consumer, db, rabbitmq, redis todos em Up.


Dicas se preferir evitar caminhos relativos

  • Mover o docker-compose.yml para a raiz do projecto e mudar build: ./api etc.
    Assim corre apenas docker compose up a partir da raiz.

  • Ou, dentro de infra/docker-compose.yml, use:

    yaml
    api: build: context: ../api dockerfile: Dockerfile # opcional mas explícito

Próximo passo

Assim que confirmar que o cluster sobe sem erros:

  1. Teste a API:

    bash
    curl -X PUT -H "Content-Type: application/json" \ -d '{"data":{"key":"foo","value":"bar"}}' http://localhost:8000/kv curl http://localhost:8000/kv?key=foo
  2. Veja os logs do consumer para validar que processa as filas:

    bash
    docker compose -f infra/docker-compose.yml logs -f consumer

Se tudo ok, diga-me e avançamos para a próxima pasta (Redis cluster + Nginx LB ou os scripts de testes de carga – como preferir).

Pergunta

#19 DONE 1.5s

#20 [consumer] resolving provenance for metadata file
#20 DONE 0.0s
api Built
consumer Built
Network infra_default Creating
Network infra_default Created
Container infra-redis-1 Creating
Container infra-db-1 Creating
Container infra-rabbitmq-1 Creating
Container infra-db-1 Created
Container infra-rabbitmq-1 Created
Container infra-consumer-1 Creating
Container infra-redis-1 Created
Container infra-api-1 Creating
Container infra-consumer-1 Created
Container infra-api-1 Created
Container infra-rabbitmq-1 Starting
Container infra-redis-1 Starting
Container infra-db-1 Starting
Container infra-db-1 Started
Container infra-redis-1 Started
Container infra-rabbitmq-1 Started
Container infra-api-1 Starting
Container infra-consumer-1 Starting
Container infra-consumer-1 Started
Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "uvicorn": executable file not found in $PATH: unknown

Pensamento

Debugging container start issue

It looks like the container failed to start because uvicorn wasn't found in the path, despite the build being successful earlier. The issue likely stems from a missing requirements.txt file in the API container. I'll suggest fixing the Dockerfile and adding uvicorn as a requirement. The user should include FastAPI and uvicorn in the requirements.txt and ensure that it’s installed either using pip or run with python -m uvicorn. I'll instruct them to adjust for that.

Thought for 28 seconds

Resposta

🚑 O api não arrancou porque o binário uvicorn não existe dentro da imagem
O erro do Docker diz exatamente isso:

exec: "uvicorn": executable file not found in $PATH

Isso acontece quando:

  1. uvicorn não foi instalado (faltou na requirements.txt ou no pip install);
  2. O CMD/ENTRYPOINT do Dockerfile chama “uvicorn” diretamente, mas o pacote não está lá.

1 ▸ Crie/actualize a pasta api/

text
distributed-kv/ └── api/ ├── main.py ├── requirements.txt └── Dockerfile

api/requirements.txt

text
fastapi==0.111.0 uvicorn[standard]==0.29.0 # servidor ASGI pika==1.3.2 # para publicar nas filas redis==5.0.4 # ler cache psycopg[binary]==3.1.18 # Cockroach/Postgres python-dotenv==1.0.1

Se quiser versões diferentes, sinta-se livre – mas inclua sempre uvicorn.


api/main.py – versão mínima já funcional

python
import os, json, datetime import asyncio import redis, pika, psycopg from fastapi import FastAPI, HTTPException, status, Query, Body from pydantic import BaseModel app = FastAPI(title="Distributed-KV API") # ─── Config ─────────────────────────────────────────────────────────────── REDIS_HOST = os.getenv("REDIS_HOST", "redis") AMQP_HOST = os.getenv("AMQP_HOST" , "rabbitmq") DB_HOST = os.getenv("DB_HOST" , "db") DB_PORT = int(os.getenv("DB_PORT", 26257)) redis_cli = redis.Redis(host=REDIS_HOST, decode_responses=True) pg = psycopg.connect( f"postgresql://root@{DB_HOST}:{DB_PORT}/kv", autocommit=True ) with pg.cursor() as cur: cur.execute("""CREATE TABLE IF NOT EXISTS kv_store( key TEXT PRIMARY KEY, value TEXT, last_updated TIMESTAMPTZ DEFAULT now() )""") # RabbitMQ – canal único a reusar mq_conn = pika.BlockingConnection(pika.ConnectionParameters(host=AMQP_HOST)) mq_chan = mq_conn.channel() mq_chan.queue_declare("add_key") mq_chan.queue_declare("del_key") # ─── Modelos ────────────────────────────────────────────────────────────── class KVPayload(BaseModel): key: str value: str # ─── End-points ──────────────────────────────────────────────────────────── @app.get("/kv", summary="Obter value por key") def get_kv(key: str = Query(...)): # primeiro tenta Redis val = redis_cli.get(key) if val is not None: return {"data": {"value": val, "source": "redis"}} # senão BD with pg.cursor() as cur: cur.execute("SELECT value FROM kv_store WHERE key = %s", (key,)) row = cur.fetchone() if row: redis_cli.set(key, row[0]) return {"data": {"value": row[0], "source": "db"}} raise HTTPException(status_code=404, detail="Key not found") @app.put("/kv", status_code=status.HTTP_202_ACCEPTED, summary="Colocar/atualizar key") def put_kv(payload: KVPayload = Body(...)): msg = { "key_name": payload.key, "key_value": payload.value, "timestamp": datetime.datetime.utcnow().isoformat() } mq_chan.basic_publish("", "add_key", json.dumps(msg).encode()) return {"queued": "add_key"} @app.delete("/kv", status_code=status.HTTP_202_ACCEPTED, summary="Apagar key") def del_kv(key: str = Query(...)): msg = {"key_name": key, "timestamp": datetime.datetime.utcnow().isoformat()} mq_chan.basic_publish("", "del_key", json.dumps(msg).encode()) return {"queued": "del_key"}

api/Dockerfile

dockerfile
FROM python:3.12-slim # ➊ instalar dependências Python WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # ➋ copiar código COPY . . # ➌ arrancar servidor ASGI CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

(Se preferir pôr outras flags – --workers, --reload em dev – fique à vontade.)


2 ▸ Reconstrua e suba

bash
./scripts/start.sh # ele já faz o --build docker compose -f infra/docker-compose.yml logs -f api

Você deve ver algo como:

text
INFO: Started server process [1] INFO: Uvicorn running on http://0.0.0.0:8000

3 ▸ Teste rápido

bash
curl -X PUT -H "Content-Type: application/json" \ -d '{"key":"foo","value":"bar"}' http://localhost:8000/kv curl http://localhost:8000/kv?key=foo curl -X DELETE http://localhost:8000/kv?key=foo

Nos logs do consumer deverá aparecer o processamento das filas (add_key / del_key).


Próxima pasta?

  • Agora que api e consumer estão vivos, escolha o próximo passo:
    • Redis cluster + Nginx load-balancer (para escalar a cache e pôr LB em frente às APIs)
    • tests/ (scripts k6/Locust + relatório)

É só dizer qual atacamos primeiro! 🚀

Pergunta

docker compose -f infra/docker-compose.yml logs -f api
api-1 | Traceback (most recent call last):
api-1 | File "/usr/local/bin/uvicorn", line 8, in <module>
api-1 | sys.exit(main())
api-1 | ^^^^^^
api-1 | File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1161, in call
api-1 | return self.main(*args, **kwargs)
api-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^
api-1 | File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1082, in main
api-1 | rv = self.invoke(ctx)
api-1 | ^^^^^^^^^^^^^^^^
api-1 | File "/usr/local/lib/python3.12/site-packages/click/core.py", line 1443, in invoke
api-1 | return ctx.invoke(self.callback, **ctx.params)
api-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
api-1 | File "/usr/local/lib/python3.12/site-packages/click/core.py", line 788, in invoke
api-1 | return __callback(*args, **kwargs)
api-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
api-1 | File "/usr/local/lib/python3.12/site-packages/uvicorn/main.py", line 409, in main
api-1 | run(
api-1 | File "/usr/local/lib/python3.12/site-packages/uvicorn/main.py", line 575, in run
api-1 | server.run()
api-1 | File "/usr/local/lib/python3.12/site-packages/uvicorn/server.py", line 65, in run
api-1 | return asyncio.run(self.serve(sockets=sockets))
api-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
api-1 | File "/usr/local/lib/python3.12/asyncio/runners.py", line 195, in run
api-1 | return runner.run(main)
api-1 | ^^^^^^^^^^^^^^^^
api-1 | File "/usr/local/lib/python3.12/asyncio/runners.py", line 118, in run
api-1 | return self._loop.run_until_complete(task)
api-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
api-1 | File "uvloop/loop.pyx", line 1518, in uvloop.loop.Loop.run_until_complete
api-1 | File "/usr/local/lib/python3.12/site-packages/uvicorn/server.py", line 69, in serve
api-1 | await self._serve(sockets)
api-1 | File "/usr/local/lib/python3.12/site-packages/uvicorn/server.py", line 76, in _serve
api-1 | config.load()
api-1 | File "/usr/local/lib/python3.12/site-packages/uvicorn/config.py", line 433, in load
api-1 | self.loaded_app = import_from_string(self.app)
api-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
api-1 | File "/usr/local/lib/python3.12/site-packages/uvicorn/importer.py", line 19, in import_from_string
api-1 | module = importlib.import_module(module_str)
api-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
api-1 | File "/usr/local/lib/python3.12/importlib/init.py", line 90, in import_module
api-1 | return _bootstrap._gcd_import(name[level:], package, level)
api-1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
api-1 | File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
api-1 | File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
api-1 | File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
api-1 | File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
api-1 | File "<frozen importlib._bootstrap_external>", line 999, in exec_module
api-1 | File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
api-1 | File "/app/main.py", line 22, in <module>
api-1 | cur.execute("""CREATE TABLE IF NOT EXISTS kv_store(
api-1 | File "/usr/local/lib/python3.12/site-packages/psycopg/cursor.py", line 732, in execute
api-1 | raise ex.with_traceback(None)
api-1 | psycopg.errors.InvalidName: no database or schema specified

Pensamento

Fixing DB connection issue

It looks like the issue is that the database wasn’t created on startup. The code is trying to connect to "kv," but in Cockroach DB, the default DB is "defaultdb." We need to either modify the connection string to use "defaultdb" or create the "kv" database manually. The quickest way is to run a command in the database container to create the "kv" database or adjust the connection string to use "defaultdb." Let’s update that!

Thought for 26 seconds

Resposta

⚠️ A API falhou porque a base de dados “kv” ainda não existe no CockroachDB
O Cockroach arrancou vazio (start-single-node --insecure) e cria apenas a BD defaultdb.
Quando o main.py abre a ligação para …/kv, o driver devolve psycopg.errors.InvalidName: no database or schema specified.


➊ Correção rápida (já em containers a correr)

bash
# entre no nó Cockroach e crie a BD docker exec -it infra-db-1 cockroach sql --insecure -e "CREATE DATABASE kv;"

Assim que a base existir, faça docker compose restart api consumer e tudo deve subir sem erros.


➋ Alternativa “sempre funciona”: inicializar a BD no start-up

Edite infra/docker-compose.yml para acrescentar um init command:

yaml
db: image: cockroachdb/cockroach:v24.1.0 command: > bash -c " cockroach start-single-node --insecure --listen-addr=0.0.0.0 && sleep 3 && cockroach sql --insecure -e 'CREATE DATABASE IF NOT EXISTS kv;'"

(mantém tudo no mesmo serviço, sem sidecar extra)


➌ Ou ainda: ligar primeiro a defaultdb e criar a “kv” via código

Atualize api/main.py (mesma lógica vale para consumer/worker.py):

python
DB_NAME = os.getenv("DB_NAME", "kv") # default “kv” dsn_root = f"postgresql://root@{DB_HOST}:{DB_PORT}/defaultdb" with psycopg.connect(dsn_root, autocommit=True) as conn: with conn.cursor() as cur: cur.execute(f"CREATE DATABASE IF NOT EXISTS {DB_NAME}") dsn_app = f"postgresql://root@{DB_HOST}:{DB_PORT}/{DB_NAME}" pg = psycopg.connect(dsn_app, autocommit=True)

Dessa forma o serviço cria a BD na primeira execução e as próximas ligações usam-na normalmente.

Se preferir parametrizar tudo, declare DB_NAME=kv nas variáveis de ambiente do api e consumer.


Teste rápido após a correção

bash
curl -X PUT -H "Content-Type: application/json" \ -d '{"key":"foo","value":"bar"}' http://localhost:8000/kv curl http://localhost:8000/kv?key=foo # deve devolver {"data":{"value":"bar","source":"redis"}}

No docker compose logs -f api consumer não deverá mais aparecer o InvalidName.


Quando confirmar que a API responde, diga se avançamos para:

  • Redis cluster + Nginx LB
  • Scripts de carga em /tests (k6 ou Locust)

Estou a postos para o próximo passo! 🚀

Compartilhar esta Q&A