Skip to content

Commit

Permalink
Feat/replace orm for psycopg (#1)
Browse files Browse the repository at this point in the history
* feat: trim project and fix admin

* feat: replace django orm for query raw

* feat: use psycopg connection pool
  • Loading branch information
rafaelpadovezi committed Mar 5, 2024
1 parent 7e3edd7 commit 05561dc
Show file tree
Hide file tree
Showing 15 changed files with 146 additions and 100 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,4 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
20 changes: 20 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Django",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/manage.py",
"args": [
"runserver",
"5047"
],
"django": true,
"autoStartBrowser": false
}
]
}
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,18 @@ Versão Python, Django e PostgreSQL da [rinha de backend 2ª edição - 2024/Q1]
- PostgreSQL
- nginx

## Running the project
## Executando o projeto

```sh
python manage.py runserver 5047
```
```bash
docker compose up api1 api2 nginx
```

## Resultados dos testes locais com gatling

![Métricas do gatling](docs/gatling.png)


## Outras versões da rinha

- [aspnet com EF Core e PostgreSQL](https://github.com/rafaelpadovezi/rinha-2)
- [aspnet com MongoDB](https://github.com/rafaelpadovezi/rinha-2-mongo)
2 changes: 1 addition & 1 deletion conf/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
events {
worker_connections 1024;
worker_connections 2048;
}

http {
Expand Down
6 changes: 6 additions & 0 deletions conf/postgresql.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
listen_addresses = '*'

max_connections = 200
client_encoding = utf8
default_transaction_isolation = 'read committed'
timezone = UTC
16 changes: 11 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ services:
- "5432:5432"
volumes:
- ./conf/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
- ./conf/postgresql.conf:/etc/postgresql/postgresql.conf
command: postgres -c config_file=/etc/postgresql/postgresql.conf
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: rinha
Expand All @@ -15,9 +17,8 @@ services:
deploy:
resources:
limits:
cpus: "0.4"
memory: "200MB"
command: postgres -c max_connections=500
cpus: "0.40"
memory: "250MB"
healthcheck:
test:
[
Expand Down Expand Up @@ -46,16 +47,21 @@ services:
condition: service_healthy
environment:
- DB_HOST=db
- USE_STATIC_FILE_HANDLER_FROM_WSGI=TRUE
- DATABASE_URL=host=db dbname=rinha user=postgres password=postgres
- GUNICORN_WORKERS=2
- DB_POOL_MAX_SIZE=45
- DEBUG=False
deploy:
resources:
limits:
cpus: "0.47"
memory: "165MB"
memory: "140MB"
api2:
<<: *api1
hostname: api2
ports:
- "8080:8080"
- "8081:8080"
nginx:
image: nginx:alpine
ports:
Expand Down
Binary file added docs/gatling.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 5 additions & 6 deletions gunicorn.conf.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from gevent import monkey
import multiprocessing
import os

monkey.patch_all()

workers = 4
worker_class = "gevent"
backlog = int(os.getenv("GUNICORN_BACKLOG", "2048"))
workers = int(os.getenv("GUNICORN_WORKERS", "3"))
worker_class = os.getenv("GUNICORN_WORKER_CLASS", "gevent")
worker_connections = int(os.getenv("GUNICORN_WORKER_CONNECTIONS", "100"))
14 changes: 7 additions & 7 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "rinha-2-django"
version = "0.1.0"
description = ""
authors = ["Rafael Miranda <rafaelpadovezi@gmail.com>"]
readme = "README.md"
package-mode = false

[tool.poetry.dependencies]
python = "^3.12"
Expand Down
5 changes: 2 additions & 3 deletions rinha.http
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ POST http://localhost:5047/clientes/1/transacoes
Content-Type: application/json

{
"valor": 1.2,
"tipo" : "c",
"descricao" : "descricao"
"valor": 1,
"tipo" : "c"
}
107 changes: 63 additions & 44 deletions rinha/apps/core/views.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,53 @@
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.decorators import api_view
from rinha.apps.core.models import Cliente
from rinha.apps.core.models import Transacao
from django.db import transaction
from rinha.apps.core.serializers import TransacaoSerializer
import logging
from psycopg.rows import dict_row
from psycopg_pool import ConnectionPool
from rinha.settings import DATABASE_URL
from rinha.settings import DB_POOL_MAX_SIZE

logger = logging.getLogger(__name__)


class TransacaoView(APIView):
def post(self, request):
pass

pool = ConnectionPool(DATABASE_URL, open=True, min_size=5, max_size=DB_POOL_MAX_SIZE)

@api_view(["GET"])
def get_extrato(request: Request, id: int) -> Response:
try:
cliente = Cliente.objects.get(pk=id)
except Cliente.DoesNotExist:
return Response({"message": "Cliente não encontrado"}, status=404)
transacoes = Transacao.objects.order_by("-id").filter(cliente__id=id)[:10]
ultimas_transacoes = []
for transacao in transacoes:
ultimas_transacoes.append(
{
"valor": transacao.valor,
"tipo": transacao.tipo,
"descricao": transacao.descricao,
"realizada_em": transacao.realizada_em,
}
)
cliente = None
with pool.connection() as conn:
with conn.cursor(row_factory=dict_row) as cur:
cur.execute("""
SELECT c.limite, c.saldo, t.*
FROM core_cliente c
LEFT JOIN (
SELECT *
FROM core_transacao
ORDER BY id DESC
LIMIT 10
) t ON c.id = t.cliente_id
WHERE c.id = %s;""", [id])
for record in cur:
if cliente is None:
cliente = {
"limite": record["limite"],
"saldo": record["saldo"],
}
if record["valor"] is not None:
ultimas_transacoes.append({
"valor": record["valor"],
"tipo": record["tipo"],
"descricao": record["descricao"],
"realizado_em": record["realizada_em"],
})
if cliente is None:
return Response({"message": "Cliente não encontrado"}, status=404)

return Response(
{
"saldo": {
"limite": cliente.limite,
"total": cliente.saldo,
"limite": cliente["limite"],
"total": cliente["saldo"],
},
"ultimas_transacoes": ultimas_transacoes,
}
Expand All @@ -47,26 +56,36 @@ def get_extrato(request: Request, id: int) -> Response:

@api_view(["POST"])
def create_transacao(request: Request, id: int) -> Response:
serializer = TransacaoSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=422)

transacao = serializer.validated_data
transacao = request.data
tipo = transacao.get("tipo")
valor = transacao.get("valor")
descricao = transacao.get("descricao")
if tipo not in ["c", "d"]:
return Response({"message": "Tipo inválido"}, status=422)
if not isinstance(valor, int) or valor < 1:
return Response({"message": "Valor inválido"}, status=422)
if descricao is None or not (1 <= len(descricao) <= 10):
return Response({"message": "Descrição inválida"}, status=422)
valor_transacao = (
transacao["valor"] if transacao["tipo"] == "c" else transacao["valor"] * -1
)

with transaction.atomic():
cliente = Cliente.objects.select_for_update().get(pk=id)
if cliente.saldo + valor_transacao < cliente.limite * -1:
return Response({"message": "Saldo insuficiente"}, status=422)
with pool.connection() as conn:

with conn.cursor(row_factory=dict_row) as cur:
cur.execute("SELECT c.limite, c.saldo FROM core_cliente c WHERE c.id = %s FOR UPDATE", [id])
result = cur.fetchone()
cliente = {
"limite": result["limite"],
"saldo": result["saldo"],
}
novo_saldo = cliente["saldo"] + valor_transacao
if novo_saldo < cliente["limite"] * -1:
return Response({"message": "Saldo insuficiente"}, status=422)
cur.execute("UPDATE core_cliente SET saldo = %s WHERE id = %s", (novo_saldo, id))
cur.execute(
"""INSERT INTO core_transacao (cliente_id, valor, tipo, descricao, realizada_em)
VALUES (%s, %s, %s, %s, 'now');
""", (id, transacao["valor"], transacao["tipo"], transacao["descricao"]))

cliente.saldo += valor_transacao
cliente.save()
Transacao.objects.create(
cliente=cliente,
valor=transacao["valor"],
tipo=transacao["tipo"],
descricao=transacao["descricao"],
)
return Response({"saldo": cliente.saldo, "limite": cliente.limite})
return Response({"saldo": novo_saldo, "limite": cliente["limite"]})
34 changes: 8 additions & 26 deletions rinha/settings.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
"""
Django settings for rinha project.
Generated by 'django-admin startproject' using Django 5.0.2.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""

import os
from pathlib import Path

Expand All @@ -24,7 +12,7 @@
SECRET_KEY = "django-insecure-k!x96799qq7@lcd(9l2ra*4+p29bw2%024)nha$3n^*bjzdw@)"

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG = os.getenv("DEBUG", True)

ALLOWED_HOSTS = ["*"]

Expand All @@ -38,17 +26,17 @@
},
"root": {
"handlers": ["console"],
"level": "WARNING",
"level": os.getenv("DJANGO_LOG_LEVEL", "ERROR"),
},
"loggers": {
"django": {
"handlers": ["console"],
"level": os.getenv("DJANGO_LOG_LEVEL", "WARNING"),
"level": os.getenv("DJANGO_LOG_LEVEL", "ERROR"),
"propagate": False,
},
"django.db.backends": {
"handlers": ["console"],
"level": "WARNING",
"level": os.getenv("DJANGO_LOG_LEVEL", "ERROR"),
"propagate": True,
},
},
Expand All @@ -59,22 +47,12 @@

INSTALLED_APPS = [
"rinha.apps.core",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "rinha.urls"
Expand Down Expand Up @@ -153,3 +131,7 @@
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

USE_STATIC_FILE_HANDLER_FROM_WSGI = os.getenv("USE_STATIC_FILE_HANDLER_FROM_WSGI", False)
DATABASE_URL = os.getenv("DATABASE_URL", "host=localhost dbname=rinha user=postgres password=postgres")
DB_POOL_MAX_SIZE = int(os.getenv("DB_POOL_MAX_SIZE", "15"))
Loading

0 comments on commit 05561dc

Please sign in to comment.