Olá! 🚀 Recebi o enunciado completo da Atividade ...
Criado em: 22 de maio de 2025
Criado em: 22 de maio de 2025
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).
Stack: Python 3.12 · FastAPI · Redis (cache) · RabbitMQ (queue) · SQLite/CockroachDB (storage) · Nginx (LB) · Docker Compose
bashgit 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
🚧 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.
Semana | Entregável | Conteúdo-chave |
---|---|---|
hoje | Milestone 1 – Skeleton | Estrutura de diretórios, docker-compose.yml mínimo, start.sh , README rascunho |
+2 d | Milestone 2 – API & Consumer | FastAPI (Python 3.12) + health-checks, RabbitMQ publish/consume, SQLite fallback |
+4 d | Milestone 3 – Cache & LB | Redis cluster + Nginx LB (round-robin, timeouts) |
+5 d | Milestone 4 – Escalabilidade | Variáveis de escala (--scale api=3 consumer=3 ), readiness probes, autoscale notes |
+6 d | Milestone 5 – Testes de carga | Scripts k6/Locust, target ≤ 1 s por request, retries=5 |
+7 d | Milestone 6 – Docs finais | Swagger/OpenAPI, relatório, diagrama arquitetura, guia cloud-deploy |
(Se precisar encurtar, juntamos 3 + 4 ou 5 + 6.)
textdistributed-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
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."
infra/docker-compose.yml
– esqueleto mínimoO slide “Exemplo – API para key-value (Ficheiro compose)” (p. 13, Aula 12) já mostra um template . A versão inicial reduzida:
yamlversion: "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) *
./scripts/start.sh
, docker compose logs
etc.) e dá feedback.Isso garante que nunca entrego um dump gigante e você controla o ritmo.
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. 😉
Ja criei essas 2
Proximas pastas
consumer/
– Worker que processa as filas RabbitMQ 🛠️Siga estes três ficheiros (coloque-os dentro de distributed-kv/consumer/
):
Dockerfile
dockerfileFROM 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"]
requirements.txt
textpika==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
worker.py
pythonimport 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.
Suba o cluster (na raiz do projeto):
bash./scripts/start.sh # já existente docker compose logs -f consumer
Publique uma chave:
bashcurl -X PUT -H "Content-Type: application/json" \ -d '{"data":{"key":"foo","value":"bar"}}' http://localhost:8000/kv
Verifique no log do consumer
que a mensagem add_key
foi processada e o valor guardado em DB + Redis.
Apague:
bashcurl -X DELETE "http://localhost:8000/kv?key=foo"
O consumer
processará del_key
e retirará a entrada.
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. 😉
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
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
docker compose
falhouO 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
:
textinfra/docker-compose.yml build: ../consumer # caminho relativo à pasta infra
Se o ficheiro não existir (ou está vazio) o build falha.
Confirme os caminhos
bashtree -L 2 # deve mostrar: # distributed-kv/ # ├── api/ # │ └── Dockerfile # ├── consumer/ # │ └── Dockerfile # └── infra/ # └── docker-compose.yml
Crie/preencha o Dockerfile
em cada serviço
api/Dockerfile (min version – FastAPI + Uvicorn):
dockerfileFROM 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):
dockerfileFROM 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"]
Garanta que o nome do ficheiro é exactamente Dockerfile
(sem extensão, sem maiúsculas diferentes).
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.
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:
yamlapi: build: context: ../api dockerfile: Dockerfile # opcional mas explícito
Assim que confirmar que o cluster sobe sem erros:
Teste a API:
bashcurl -X PUT -H "Content-Type: application/json" \ -d '{"data":{"key":"foo","value":"bar"}}' http://localhost:8000/kv curl http://localhost:8000/kv?key=foo
Veja os logs do consumer para validar que processa as filas:
bashdocker 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).
#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
🚑 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:
uvicorn
não foi instalado (faltou na requirements.txt
ou no pip install
);CMD
/ENTRYPOINT
do Dockerfile chama “uvicorn” diretamente, mas o pacote não está lá.api/
textdistributed-kv/ └── api/ ├── main.py ├── requirements.txt └── Dockerfile
api/requirements.txt
textfastapi==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á funcionalpythonimport 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
dockerfileFROM 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.)
bash./scripts/start.sh # ele já faz o --build docker compose -f infra/docker-compose.yml logs -f api
Você deve ver algo como:
textINFO: Started server process [1] INFO: Uvicorn running on http://0.0.0.0:8000
bashcurl -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
).
É só dizer qual atacamos primeiro! 🚀
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
⚠️ 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.
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.
Edite infra/docker-compose.yml para acrescentar um init command:
yamldb: 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)
Atualize api/main.py (mesma lógica vale para consumer/worker.py
):
pythonDB_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.
bashcurl -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:
Estou a postos para o próximo passo! 🚀