├── containerization
├── replicar-db_init.sql.sh
├── run.sh
├── build.sh
├── nginx
│ ├── nginx.conf
│ └── docker-compose.yml
├── haproxy
│ ├── haproxy.cfg
│ └── docker-compose.yml
├── nginx-hostmode
│ ├── nginx.conf
│ └── docker-compose.yml
├── db
│ ├── docker-compose.yml
│ └── init.sql
├── clean.sh
└── testes-jvm
├── Dockerfile
├── participacao
├── nginx.conf
├── README.md
├── init.sql
└── docker-compose.yml
├── src
└── rinha_2024q1_crebito
│ ├── payloads.clj
│ ├── middlewares.clj
│ ├── db.clj
│ ├── endpoints.clj
│ └── http_handlers.clj
├── test
└── rinha_2024q1_crebito
│ └── handler_test.clj
├── project.clj
├── requests.http
├── README.md
└── .gitignore
/containerization/replicar-db_init.sql.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | cp ./db/init.sql ./nginx/
4 | cp ./db/init.sql ./haproxy/
5 | cp ./db/init.sql ./nginx-hostmode/
6 |
--------------------------------------------------------------------------------
/containerization/run.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | # exemplo de uso: `./run.sh nginx`
4 |
5 | docker compose -f ./$1/docker-compose.yml down
6 | docker compose -f ./$1/docker-compose.yml up
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:23-slim
2 |
3 | ADD target/rinha-2024q1-crebito-0.1.0-SNAPSHOT-standalone.jar /rinha-2024q1-crebito/app.jar
4 |
5 | EXPOSE 3000
6 |
7 | CMD ["java", "-jar", "/rinha-2024q1-crebito/app.jar"]
8 |
--------------------------------------------------------------------------------
/containerization/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | pushd ../
4 | lein ring uberjar
5 | docker build -t rinha-2024q1-crebito -t zanfranceschi/rinha-2024q1-crebito .
6 | docker push zanfranceschi/rinha-2024q1-crebito
7 | popd
8 |
--------------------------------------------------------------------------------
/participacao/nginx.conf:
--------------------------------------------------------------------------------
1 | events {
2 | worker_connections 1000;
3 | }
4 |
5 | http {
6 | access_log off;
7 | sendfile on;
8 |
9 | upstream api {
10 | server api01:3000;
11 | server api02:3000;
12 | }
13 |
14 | server {
15 | listen 9999;
16 |
17 | location / {
18 | proxy_pass http://api;
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/containerization/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | events {
2 | worker_connections 1000;
3 | }
4 |
5 | http {
6 | access_log off;
7 | sendfile on;
8 |
9 | upstream api {
10 | server api01:3000;
11 | server api02:3000;
12 | }
13 |
14 | server {
15 | listen 9999;
16 |
17 | location / {
18 | proxy_pass http://api;
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/containerization/haproxy/haproxy.cfg:
--------------------------------------------------------------------------------
1 | global
2 | maxconn 1000
3 |
4 | defaults
5 | mode http
6 | timeout client 30s
7 | timeout connect 30s
8 | timeout server 30s
9 | timeout http-request 30s
10 | log global
11 |
12 | frontend myfrontend
13 | bind :9999
14 | default_backend apis
15 |
16 | backend apis
17 | balance roundrobin
18 | server s1 api01:3000 check
19 | server s2 api02:3000 check
20 |
--------------------------------------------------------------------------------
/containerization/nginx-hostmode/nginx.conf:
--------------------------------------------------------------------------------
1 | events {
2 | worker_connections 1000;
3 | }
4 |
5 | http {
6 | access_log off;
7 | sendfile on;
8 |
9 | upstream api {
10 | server localhost:3001;
11 | server localhost:3002;
12 | }
13 |
14 | server {
15 | listen 9999;
16 |
17 | location / {
18 | proxy_pass http://api;
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/containerization/db/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.5"
2 |
3 | services:
4 | db:
5 | image: postgres:latest
6 | hostname: db
7 | environment:
8 | - POSTGRES_PASSWORD=123
9 | - POSTGRES_USER=admin
10 | - POSTGRES_DB=rinha
11 | expose:
12 | - "5432"
13 | volumes:
14 | - ./init.sql:/docker-entrypoint-initdb.d/init.sql
15 | network_mode: host
16 | deploy:
17 | resources:
18 | limits:
19 | cpus: "0.35"
20 | memory: "200MB"
21 |
--------------------------------------------------------------------------------
/src/rinha_2024q1_crebito/payloads.clj:
--------------------------------------------------------------------------------
1 | (ns rinha-2024q1-crebito.payloads
2 | (:require [schema.core :as s]))
3 |
4 | ;; - 2.147.483.647 é o valor máximo para INTEGER do Postgres
5 | ;; - Com créditos demais, pode haver erros de out of range.
6 | ;; (Não validado propositalmente)
7 | (s/defschema Transacao
8 | {:valor (s/pred #(and (pos-int? %) (<= % 1000000)))
9 | :tipo (s/enum "c" "d")
10 | :descricao (s/pred (fn [d]
11 | (and (string? d)
12 | (<= 1 (count d) 10))))})
13 |
--------------------------------------------------------------------------------
/test/rinha_2024q1_crebito/handler_test.clj:
--------------------------------------------------------------------------------
1 | (ns rinha-2024q1-crebito.handler-test
2 | (:require [clojure.test :refer [deftest is testing]]
3 | [ring.mock.request :as mock]
4 | [rinha-2024q1-crebito.endpoints :refer [app]]))
5 |
6 | (deftest test-app
7 | (testing "main route"
8 | (let [response (app (mock/request :get "/"))]
9 | (is (= (:status response) 200))
10 | (is (= (:body response) "Hello World"))))
11 |
12 | (testing "not-found route"
13 | (let [response (app (mock/request :get "/invalid"))]
14 | (is (= (:status response) 404)))))
15 |
--------------------------------------------------------------------------------
/containerization/clean.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | (
4 | docker-compose -f ./db/docker-compose.yml rm -f
5 | docker-compose -f ./db/docker-compose.yml down --rmi all
6 | docker-compose -f ./nginx/docker-compose.yml rm -f
7 | docker-compose -f ./nginx/docker-compose.yml down --rmi all
8 | docker-compose -f ./haproxy/docker-compose.yml rm -f
9 | docker-compose -f ./haproxy/docker-compose.yml down --rmi all
10 | docker-compose -f ./nginx-hostmode/docker-compose.yml rm -f
11 | docker-compose -f ./nginx-hostmode/docker-compose.yml down --rmi all
12 | docker system prune -f
13 | docker rmi $(docker images -a -q)
14 | )
--------------------------------------------------------------------------------
/src/rinha_2024q1_crebito/middlewares.clj:
--------------------------------------------------------------------------------
1 | (ns rinha-2024q1-crebito.middlewares
2 | (:require [next.jdbc :as jdbc]
3 | [rinha-2024q1-crebito.db :as db]))
4 |
5 | (def clientes
6 | (memoize (fn []
7 | (reduce (fn [acc {:clientes/keys [id nome limite]}]
8 | (assoc acc id {:id id :nome nome :limite limite})) {}
9 | (jdbc/execute! db/spec ["select * from clientes"])))))
10 |
11 | (defn wrap-db
12 | [handler]
13 | (fn [request]
14 | (handler (assoc request :db-spec db/spec))))
15 |
16 | (defn wrap-clientes
17 | [handler]
18 | (fn [request]
19 | (handler (assoc request :cached-clientes (clientes)))))
20 |
--------------------------------------------------------------------------------
/src/rinha_2024q1_crebito/db.clj:
--------------------------------------------------------------------------------
1 | (ns rinha-2024q1-crebito.db
2 | (:require [jdbc.pool.c3p0 :as pool]))
3 |
4 | (def db-hostname (or (System/getenv "DB_HOSTNAME") "localhost"))
5 | (def db-initial-pool-size (Integer/parseInt (or (System/getenv "DB_INITIAL_POOL_SIZE") "3")))
6 | (def db-max-pool-size (Integer/parseInt (or (System/getenv "DB_MAX_POOL_SIZE") "15")))
7 |
8 | (def spec
9 | (pool/make-datasource-spec
10 | {:classname "org.postgresql.Driver"
11 | :subprotocol "postgresql"
12 | :user "admin"
13 | :password "123"
14 | :subname (str "//" db-hostname ":5432/rinha?ApplicationName=rinha-web-server")
15 | :initial-pool-size db-initial-pool-size
16 | :max-pool-size db-max-pool-size}))
17 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject rinha-2024q1-crebito "0.1.0-SNAPSHOT"
2 | :description "FIXME: write description"
3 | :url "http://example.com/FIXME"
4 | :min-lein-version "2.0.0"
5 | :dependencies [[org.clojure/clojure "1.10.0"]
6 | [compojure "1.6.1"]
7 | [ring/ring-defaults "0.3.2"]
8 | [ring/ring-json "0.5.1"]
9 | [org.postgresql/postgresql "42.6.0"]
10 | [clojure.jdbc/clojure.jdbc-c3p0 "0.3.4"]
11 | [com.github.seancorfield/next.jdbc "1.3.909"]
12 | [prismatic/schema "1.4.1"]]
13 | :plugins [[lein-ring "0.12.5"]]
14 | :ring {:handler rinha-2024q1-crebito.endpoints/app}
15 | :profiles
16 | {:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
17 | [ring/ring-mock "0.3.2"]]}
18 | :uberjar {:aot :all}})
19 |
--------------------------------------------------------------------------------
/participacao/README.md:
--------------------------------------------------------------------------------
1 | # Submissão para Rinha de Backend, Segunda Edição: 2024/Q1 - Controle de Concorrência
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ## Francisco Zanfranceschi
11 | Submissão feita com:
12 | - `nginx` como load balancer
13 | - `postgres` como banco de dados
14 | - `clojure` para api com as libs `compojure` e `next.jdbc`
15 | - [repositório da api](https://github.com/zanfranceschi/rinha-de-backend-2024-q1-poc)
16 |
17 | [@zanfranceschi](https://twitter.com/zanfranceschi) @ twitter
18 |
--------------------------------------------------------------------------------
/requests.http:
--------------------------------------------------------------------------------
1 | GET http://localhost:9999/clientes/1/extrato
2 |
3 |
4 | ###
5 | GET http://localhost:9999/clientes/1/extrato
6 |
7 |
8 | ###
9 | POST http://localhost:9999/admin/db-reset
10 |
11 |
12 | ###
13 | POST http://localhost:9999/clientes/1/transacoes
14 | Content-Type: application/json
15 |
16 | {
17 | "valor": 1,
18 | "tipo": "c",
19 | "descricao" : "descrição"
20 | }
21 |
22 |
23 | ###
24 | POST http://localhost:9999/clientes/1/transacoes
25 | Content-Type: application/json
26 |
27 | {
28 | "valor": 100000,
29 | "tipo": "c",
30 | "descricao" : "descrição"
31 | }
32 |
33 | ###
34 |
35 | POST http://localhost:9999/clientes/1/transacoes
36 | Content-Type: application/json
37 |
38 | {
39 | "valor": 1000,
40 | "tipo": "c",
41 | "descricao" : "pix"
42 | }
43 |
44 |
45 | file:///home/zanfranceschi/projects/rinha-de-backend-2024-q1/load-test/user-files/results/rinhabackendcrebitossimulation-20240210024319314/index.html
--------------------------------------------------------------------------------
/src/rinha_2024q1_crebito/endpoints.clj:
--------------------------------------------------------------------------------
1 | (ns rinha-2024q1-crebito.endpoints
2 | (:require [compojure.core :refer [defroutes GET POST]]
3 | [compojure.route :as route]
4 | [ring.middleware.defaults :refer [site-defaults wrap-defaults]]
5 | [ring.middleware.json :as json]
6 | [rinha-2024q1-crebito.http-handlers :as handlers]
7 | [rinha-2024q1-crebito.middlewares :as middlewares]))
8 |
9 | (defroutes app-routes
10 | (GET ["/"] _ "ok")
11 | (GET ["/clientes/:id/extrato" :id #"[0-9]+"] _ handlers/extrato!)
12 | (POST ["/clientes/:id/transacoes" :id #"[0-9]+"] _ handlers/transacionar!)
13 | (POST ["/admin/db-reset"] _ handlers/admin-reset-db!)
14 | (route/not-found "oops hehe"))
15 |
16 | (def app
17 | (wrap-defaults
18 | (-> app-routes
19 | middlewares/wrap-db
20 | middlewares/wrap-clientes
21 | (json/wrap-json-body {:keywords? true})
22 | json/wrap-json-response)
23 | (assoc-in site-defaults [:security :anti-forgery] false)))
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rinha de Backend: 2024/Q1 - Crébito
2 |
3 | POC para a segunda edição da Rinha de Backend feita em Clojure com as libs compojure e next.jdbc.
4 |
5 | ## Pré Requisitos
6 |
7 | Necessário [Leiningen][] 2.0.0 ou superior instaldo.
8 |
9 | [leiningen]: https://github.com/technomancy/leiningen
10 |
11 | ## Para Executar
12 |
13 | Para subir o servidor, execute o seguinte comando:
14 |
15 | lein ring server-headless
16 |
17 | Se quiser alterar a porta, configure a variável de ambiente `PORT` ou adicione `:port ` na chave `:ring` no arquivo [project.clj](./project.clj).
18 |
19 |
20 | ## Conteinerização
21 |
22 | O diretório [containerization](./containerization/) possui diversos exemplos para conteinerização da API. Talvez você precise fazer ajustes para que funcionem.
23 |
24 |
25 | ## Submissão
26 |
27 | O diretório [participacao](./participacao) contem os artefatos usados para a submissão da Rinha de Backend.
28 |
29 |
30 | ## License
31 |
32 | MIT - Copyright © 2024 - Poc Rinha de Backend Segunda Edição em Clojure
33 |
--------------------------------------------------------------------------------
/participacao/init.sql:
--------------------------------------------------------------------------------
1 | CREATE UNLOGGED TABLE clientes (
2 | id SERIAL PRIMARY KEY,
3 | nome VARCHAR(50) NOT NULL,
4 | limite INTEGER NOT NULL
5 | );
6 |
7 | CREATE UNLOGGED TABLE transacoes (
8 | id SERIAL PRIMARY KEY,
9 | cliente_id INTEGER NOT NULL,
10 | valor INTEGER NOT NULL,
11 | tipo CHAR(1) NOT NULL,
12 | descricao VARCHAR(10) NOT NULL,
13 | realizada_em TIMESTAMP NOT NULL DEFAULT NOW(),
14 | CONSTRAINT fk_clientes_transacoes_id
15 | FOREIGN KEY (cliente_id) REFERENCES clientes(id)
16 | );
17 |
18 | CREATE UNLOGGED TABLE saldos (
19 | id SERIAL PRIMARY KEY,
20 | cliente_id INTEGER NOT NULL,
21 | valor INTEGER NOT NULL,
22 | CONSTRAINT fk_clientes_saldos_id
23 | FOREIGN KEY (cliente_id) REFERENCES clientes(id)
24 | );
25 |
26 | DO $$
27 | BEGIN
28 | INSERT INTO clientes (nome, limite)
29 | VALUES
30 | ('o barato sai caro', 1000 * 100),
31 | ('zan corp ltda', 800 * 100),
32 | ('les cruders', 10000 * 100),
33 | ('padaria joia de cocaia', 100000 * 100),
34 | ('kid mais', 5000 * 100);
35 |
36 | INSERT INTO saldos (cliente_id, valor)
37 | SELECT id, 0 FROM clientes;
38 | END;
39 | $$;
40 |
--------------------------------------------------------------------------------
/containerization/haproxy/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.5"
2 |
3 | services:
4 | api01: &api
5 | image: zanfranceschi/rinha-2024q1-crebito
6 | hostname: api01
7 | environment:
8 | - DB_HOSTNAME=db
9 | - PORT=3000
10 | ports:
11 | - "3001:3000"
12 | depends_on:
13 | - db
14 | deploy:
15 | resources:
16 | limits:
17 | cpus: "0.45"
18 | memory: "200MB"
19 |
20 | api02:
21 | <<: *api
22 | hostname: api02
23 | environment:
24 | - DB_HOSTNAME=db
25 | - PORT=3000
26 | ports:
27 | - "3002:3000"
28 |
29 | haproxy:
30 | image: haproxy:2.9-alpine
31 | volumes:
32 | - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
33 | depends_on:
34 | - api01
35 | - api02
36 | ports:
37 | - "9999:9999"
38 | deploy:
39 | resources:
40 | limits:
41 | cpus: "0.15"
42 | memory: "10MB"
43 |
44 | db:
45 | image: postgres:latest
46 | hostname: db
47 | environment:
48 | - POSTGRES_PASSWORD=123
49 | - POSTGRES_USER=admin
50 | - POSTGRES_DB=rinha
51 | ports:
52 | - "5432:5432"
53 | volumes:
54 | - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
55 | command: postgres -c checkpoint_timeout=600 -c max_wal_size=4096
56 | deploy:
57 | resources:
58 | limits:
59 | cpus: "0.45"
60 | memory: "140MB"
61 |
62 | networks:
63 | default:
64 | driver: bridge
65 | name: rinha-haproxy-2024q1
66 |
--------------------------------------------------------------------------------
/containerization/nginx-hostmode/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.5"
2 |
3 | services:
4 | api01: &api
5 | image: zanfranceschi/rinha-2024q1-crebito
6 | hostname: api01
7 | environment:
8 | - DB_HOSTNAME=localhost
9 | - PORT=3001
10 | network_mode: "host"
11 | expose:
12 | - "3001"
13 | depends_on:
14 | - db
15 | deploy:
16 | resources:
17 | limits:
18 | cpus: "0.45"
19 | memory: "200MB"
20 |
21 | api02:
22 | <<: *api
23 | hostname: api02
24 | environment:
25 | - DB_HOSTNAME=localhost
26 | - PORT=3002
27 | network_mode: "host"
28 | expose:
29 | - "3002"
30 |
31 | nginx:
32 | image: nginx:latest
33 | volumes:
34 | - ./nginx.conf:/etc/nginx/nginx.conf:ro
35 | depends_on:
36 | - api01
37 | - api02
38 | network_mode: "host"
39 | expose:
40 | - "9999"
41 | deploy:
42 | resources:
43 | limits:
44 | cpus: "0.15"
45 | memory: "10MB"
46 |
47 | db:
48 | image: postgres:latest
49 | hostname: db
50 | environment:
51 | - POSTGRES_PASSWORD=123
52 | - POSTGRES_USER=admin
53 | - POSTGRES_DB=rinha
54 | network_mode: "host"
55 | expose:
56 | - "5432"
57 | volumes:
58 | - ../db/init.sql:/docker-entrypoint-initdb.d/init.sql
59 | command: postgres -c checkpoint_timeout=600 -c max_wal_size=4096
60 | deploy:
61 | resources:
62 | limits:
63 | cpus: "0.45"
64 | memory: "140MB"
65 |
--------------------------------------------------------------------------------
/containerization/nginx/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.5"
2 |
3 | services:
4 | api01: &api
5 | image: zanfranceschi/rinha-2024q1-crebito
6 | hostname: api01
7 | environment:
8 | - DB_HOSTNAME=db
9 | - DB_INITIAL_POOL_SIZE=15
10 | - DB_MAX_POOL_SIZE=15
11 | - API_USE_DB_FUNC=true
12 | - PORT=3000
13 | ports:
14 | - "3001:3000"
15 | depends_on:
16 | - db
17 | deploy:
18 | resources:
19 | limits:
20 | cpus: "0.55"
21 | memory: "200MB"
22 |
23 | api02:
24 | <<: *api
25 | hostname: api02
26 | ports:
27 | - "3002:3000"
28 |
29 | nginx:
30 | image: nginx:latest
31 | volumes:
32 | - ./nginx.conf:/etc/nginx/nginx.conf:ro
33 | depends_on:
34 | - api01
35 | - api02
36 | ports:
37 | - "9999:9999"
38 | deploy:
39 | resources:
40 | limits:
41 | cpus: "0.15"
42 | memory: "10MB"
43 |
44 | db:
45 | image: postgres:latest
46 | hostname: db
47 | environment:
48 | - POSTGRES_PASSWORD=123
49 | - POSTGRES_USER=admin
50 | - POSTGRES_DB=rinha
51 | ports:
52 | - "5432:5432"
53 | volumes:
54 | - ../db/init.sql:/docker-entrypoint-initdb.d/init.sql
55 | command: postgres -c checkpoint_timeout=600 -c max_wal_size=4096 -c synchronous_commit=0
56 | deploy:
57 | resources:
58 | limits:
59 | cpus: "0.25"
60 | memory: "140MB"
61 |
62 | networks:
63 | default:
64 | driver: bridge
65 | name: rinha-nginx-2024q1
66 |
--------------------------------------------------------------------------------
/participacao/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.5"
2 |
3 | services:
4 | api01: &api
5 | image: zanfranceschi/rinha-2024q1-crebito
6 | hostname: api01
7 | environment:
8 | - DB_HOSTNAME=db
9 | - USAR_DB_PROC=false
10 | - FORCAR_INCONSISTENCIA=false
11 | - PORT=3000
12 | ports:
13 | - "3001:3000"
14 | depends_on:
15 | - db
16 | deploy:
17 | resources:
18 | limits:
19 | cpus: "0.45"
20 | memory: "200MB"
21 |
22 | api02:
23 | <<: *api
24 | hostname: api02
25 | environment:
26 | - DB_HOSTNAME=db
27 | - USAR_DB_PROC=false
28 | - FORCAR_INCONSISTENCIA=false
29 | - PORT=3000
30 | ports:
31 | - "3002:3000"
32 |
33 | nginx:
34 | image: nginx:latest
35 | volumes:
36 | - ./nginx.conf:/etc/nginx/nginx.conf:ro
37 | depends_on:
38 | - api01
39 | - api02
40 | ports:
41 | - "9999:9999"
42 | deploy:
43 | resources:
44 | limits:
45 | cpus: "0.15"
46 | memory: "10MB"
47 |
48 | db:
49 | image: postgres:latest
50 | hostname: db
51 | environment:
52 | - POSTGRES_PASSWORD=123
53 | - POSTGRES_USER=admin
54 | - POSTGRES_DB=rinha
55 | ports:
56 | - "5432:5432"
57 | volumes:
58 | - ./init.sql:/docker-entrypoint-initdb.d/init.sql
59 | command: postgres -c checkpoint_timeout=600 -c max_wal_size=4096
60 | deploy:
61 | resources:
62 | limits:
63 | cpus: "0.45"
64 | memory: "140MB"
65 |
66 | networks:
67 | default:
68 | driver: bridge
69 | name: rinha-nginx-2024q1
70 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /lib
3 | /classes
4 | /checkouts
5 | pom.xml
6 | pom.xml.asc
7 | *.jar
8 | *.class
9 | /.lein-*
10 | /.nrepl-port
11 |
12 | #calva
13 | # visual code
14 | /*.vsix
15 | # .vscode/settings.json
16 | .vscode-test
17 | .lh
18 |
19 | # Logs
20 | logs
21 | *.log
22 | npm-debug.log*
23 |
24 | # Runtime data
25 | pids
26 | *.pid
27 | *.seed
28 |
29 | # Directory for instrumented libs generated by jscoverage/JSCover
30 | lib-cov
31 |
32 | # Coverage directory used by tools like istanbul
33 | coverage
34 |
35 | # nyc test coverage
36 | .nyc_output
37 |
38 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
39 | .grunt
40 |
41 | # node-waf configuration
42 | .lock-wscript
43 |
44 | # Compiled binary addons (http://nodejs.org/api/addons.html)
45 | build/Release
46 |
47 | # Dependency directories
48 | node_modules
49 | jspm_packages
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional REPL history
55 | .node_repl_history
56 |
57 | #Macos
58 | .DS_Store
59 |
60 | ## CLJS
61 | .shadow-cljs/
62 | lib/
63 | cljs-out/
64 | test-out/
65 |
66 | # This and that
67 | .nrepl-port
68 | dist/
69 | pom.xml
70 | .idea
71 | *.iml
72 | *.bak
73 |
74 | ## TS
75 | out/
76 | html/main.js
77 | tsconfig.tsbuildinfo
78 | html/main.js.map
79 |
80 | # Test reporter
81 | junit/test-results.xml
82 |
83 | # Don't know what this is
84 | report.*
85 |
86 | # clojure-lsp
87 | .lsp
88 | output.calva-repl
89 | .clj-kondo
90 | /clojure-lsp*
91 |
92 | # MkDocs
93 | /site/
94 | .cache/
95 |
96 | # Calva dev
97 | clojure.tmLanguage.json
98 | .cpcache/
99 | backup/
100 |
101 | # Calva grammars (running the atom-ci docker image locally)
102 | src/calva-fmt/atom-language-clojure/.cache/
103 | src/calva-fmt/atom-language-clojure/.bash_history
104 |
105 | # Leiningen
106 | .lein-repl-history
107 |
108 | # Portal
109 | .portal
--------------------------------------------------------------------------------
/containerization/testes-jvm:
--------------------------------------------------------------------------------
1 | java -Xms200m
2 | -Xmx200m
3 | -XX:MaxMetaspaceSize=100m
4 | -XX:MaxGCPauseMillis=200
5 | -XX:ParallelGCThreads=20
6 | -XX:ConcGCThreads=5
7 | -XX:InitiatingHeapOccupancyPercent=70
8 | -Dcom.sun.management.jmxremote=true
9 | -Dcom.sun.management.jmxremote.port=8849
10 | -Dcom.sun.management.jmxremote.authenticate=false
11 | -Dcom.sun.management.jmxremote.ssl=false
12 | -Dcom.sun.management.jmxremote.local.only=false
13 | -Dcom.sun.management.jmxremote.rmi.port=8849
14 | -Djava.rmi.server.hostname=localhost
15 | -jar target/rinha-2024q1-crebito-0.1.0-SNAPSHOT-standalone.jar
16 |
17 | java -Xms200m
18 | -Xmx200m
19 | -XX:MaxMetaspaceSize=100m
20 | -XX:MaxGCPauseMillis=200
21 | -XX:ParallelGCThreads=20
22 | -XX:ConcGCThreads=5
23 | -XX:InitiatingHeapOccupancyPercent=70
24 | -Dcom.sun.management.jmxremote=true
25 | -Dcom.sun.management.jmxremote.port=8849
26 | -Dcom.sun.management.jmxremote.authenticate=false
27 | -Dcom.sun.management.jmxremote.ssl=false
28 | -Dcom.sun.management.jmxremote.local.only=false
29 | -Dcom.sun.management.jmxremote.rmi.port=8849
30 | -Djava.rmi.server.hostname=localhost
31 | -jar app.jar
32 |
33 | java -Dcom.sun.management.jmxremote=true
34 | -Dcom.sun.management.jmxremote.port=8849
35 | -Dcom.sun.management.jmxremote.authenticate=false
36 | -Dcom.sun.management.jmxremote.ssl=false
37 | -Dcom.sun.management.jmxremote.local.only=false
38 | -Dcom.sun.management.jmxremote.rmi.port=8849
39 | -Djava.rmi.server.hostname=localhost
40 | -jar app.jar
41 |
42 |
43 | java -Dcom.sun.management.jmxremote=true
44 | -Dcom.sun.management.jmxremote.port=8849
45 | -Dcom.sun.management.jmxremote.authenticate=false
46 | -Dcom.sun.management.jmxremote.ssl=false
47 | -Dcom.sun.management.jmxremote.local.only=false
48 | -Dcom.sun.management.jmxremote.rmi.port=8849
49 | -Djava.rmi.server.hostname=localhost
50 | -Xms200m
51 | -Xmx200m
52 | -XX:MaxMetaspaceSize=100m
53 | -XX:MaxGCPauseMillis=200
54 | -XX:ParallelGCThreads=20
55 | -XX:ConcGCThreads=5
56 | -XX:InitiatingHeapOccupancyPercent=70
57 | -jar /rinha-2024q1-crebito/app.jar
--------------------------------------------------------------------------------
/containerization/db/init.sql:
--------------------------------------------------------------------------------
1 | CREATE UNLOGGED TABLE clientes (
2 | id SERIAL PRIMARY KEY,
3 | nome VARCHAR(50) NOT NULL,
4 | limite INTEGER NOT NULL
5 | );
6 |
7 | CREATE UNLOGGED TABLE transacoes (
8 | id SERIAL PRIMARY KEY,
9 | cliente_id INTEGER NOT NULL,
10 | valor INTEGER NOT NULL,
11 | tipo CHAR(1) NOT NULL,
12 | descricao VARCHAR(10) NOT NULL,
13 | realizada_em TIMESTAMP NOT NULL DEFAULT NOW(),
14 | CONSTRAINT fk_clientes_transacoes_id
15 | FOREIGN KEY (cliente_id) REFERENCES clientes(id)
16 | );
17 |
18 | CREATE UNLOGGED TABLE saldos (
19 | id SERIAL PRIMARY KEY,
20 | cliente_id INTEGER NOT NULL,
21 | valor INTEGER NOT NULL,
22 | CONSTRAINT fk_clientes_saldos_id
23 | FOREIGN KEY (cliente_id) REFERENCES clientes(id)
24 | );
25 |
26 | DO $$
27 | BEGIN
28 | INSERT INTO clientes (nome, limite)
29 | VALUES
30 | ('o barato sai caro', 1000 * 100),
31 | ('zan corp ltda', 800 * 100),
32 | ('les cruders', 10000 * 100),
33 | ('padaria joia de cocaia', 100000 * 100),
34 | ('kid mais', 5000 * 100);
35 |
36 | INSERT INTO saldos (cliente_id, valor)
37 | SELECT id, 0 FROM clientes;
38 | END;
39 | $$;
40 |
41 | CREATE OR REPLACE FUNCTION debitar(
42 | cliente_id_tx INT,
43 | valor_tx INT,
44 | descricao_tx VARCHAR(10))
45 | RETURNS TABLE (
46 | novo_saldo INT,
47 | possui_erro BOOL,
48 | mensagem VARCHAR(20))
49 | LANGUAGE plpgsql
50 | AS $$
51 | DECLARE
52 | saldo_atual int;
53 | limite_atual int;
54 | BEGIN
55 | PERFORM pg_advisory_xact_lock(cliente_id_tx);
56 | SELECT
57 | c.limite,
58 | COALESCE(s.valor, 0)
59 | INTO
60 | limite_atual,
61 | saldo_atual
62 | FROM clientes c
63 | LEFT JOIN saldos s
64 | ON c.id = s.cliente_id
65 | WHERE c.id = cliente_id_tx;
66 |
67 | IF saldo_atual - valor_tx >= limite_atual * -1 THEN
68 | INSERT INTO transacoes
69 | VALUES(DEFAULT, cliente_id_tx, valor_tx, 'd', descricao_tx, NOW());
70 |
71 | UPDATE saldos
72 | SET valor = valor - valor_tx
73 | WHERE cliente_id = cliente_id_tx;
74 |
75 | RETURN QUERY
76 | SELECT
77 | valor,
78 | FALSE,
79 | 'ok'::VARCHAR(20)
80 | FROM saldos
81 | WHERE cliente_id = cliente_id_tx;
82 | ELSE
83 | RETURN QUERY
84 | SELECT
85 | valor,
86 | TRUE,
87 | 'saldo insuficente'::VARCHAR(20)
88 | FROM saldos
89 | WHERE cliente_id = cliente_id_tx;
90 | END IF;
91 | END;
92 | $$;
93 |
94 | CREATE OR REPLACE FUNCTION creditar(
95 | cliente_id_tx INT,
96 | valor_tx INT,
97 | descricao_tx VARCHAR(10))
98 | RETURNS TABLE (
99 | novo_saldo INT,
100 | possui_erro BOOL,
101 | mensagem VARCHAR(20))
102 | LANGUAGE plpgsql
103 | AS $$
104 | BEGIN
105 | PERFORM pg_advisory_xact_lock(cliente_id_tx);
106 |
107 | INSERT INTO transacoes
108 | VALUES(DEFAULT, cliente_id_tx, valor_tx, 'c', descricao_tx, NOW());
109 |
110 | RETURN QUERY
111 | UPDATE saldos
112 | SET valor = valor + valor_tx
113 | WHERE cliente_id = cliente_id_tx
114 | RETURNING valor, FALSE, 'ok'::VARCHAR(20);
115 | END;
116 | $$;
--------------------------------------------------------------------------------
/src/rinha_2024q1_crebito/http_handlers.clj:
--------------------------------------------------------------------------------
1 | (ns rinha-2024q1-crebito.http-handlers
2 | (:require [next.jdbc :as jdbc]
3 | [rinha-2024q1-crebito.payloads :as payloads]
4 | [schema.core :as s]))
5 |
6 | (defn ^:private creditar!
7 | [cliente_id clientes valor descricao db-spec]
8 | (jdbc/with-transaction [conn db-spec]
9 | (jdbc/execute! conn ["select pg_advisory_xact_lock(?)" cliente_id])
10 | (jdbc/execute! conn ["insert into transacoes
11 | (id, cliente_id, valor, tipo, descricao, realizada_em)
12 | values(default, ?, ?, 'c', ?, now())"
13 | cliente_id
14 | valor
15 | descricao])
16 | (let [{novo-saldo :saldos/saldo} (jdbc/execute-one! conn ["update saldos
17 | set valor = valor + ?
18 | where cliente_id = ?
19 | returning valor as saldo"
20 | valor
21 | cliente_id])
22 | {limite :limite} (get clientes cliente_id)]
23 | {:status 200
24 | :body {:limite limite
25 | :saldo novo-saldo}})))
26 |
27 | (defn ^:private debitar!
28 | [cliente_id clientes valor descricao db-spec]
29 | (jdbc/with-transaction [conn db-spec]
30 | (jdbc/execute-one! conn ["select pg_advisory_xact_lock(?)" cliente_id])
31 | (let [{limite :limite} (get clientes cliente_id)
32 | {saldo :saldos/saldo} (jdbc/execute-one! conn ["select valor as saldo
33 | from saldos
34 | where cliente_id = ?"
35 | cliente_id])
36 | tem-limite? (>= (- saldo valor) (* limite -1))]
37 | (if tem-limite?
38 | (let [{novo-saldo :saldos/saldo} (jdbc/execute-one! conn ["update saldos
39 | set valor = valor + ?
40 | where cliente_id = ?
41 | returning valor as saldo"
42 | (* valor -1)
43 | cliente_id])]
44 | (jdbc/execute! conn ["insert into transacoes
45 | (id, cliente_id, valor, tipo, descricao, realizada_em)
46 | values(default, ?, ?, 'd', ?, now())"
47 | cliente_id
48 | valor
49 | descricao])
50 | {:status 200
51 | :body {:limite limite
52 | :saldo novo-saldo}})
53 | {:status 422
54 | :body {:erro "limite insuficiente"}}))))
55 |
56 | (defn ^:private extrato!*
57 | [{db-spec :db-spec
58 | cliente_id :cliente-id
59 | clientes :cached-clientes}]
60 | (let [resultado (jdbc/execute!
61 | db-spec
62 | ["(select valor, 'saldo' as tipo, 'saldo' as descricao, now() as realizada_em
63 | from saldos
64 | where cliente_id = ?)
65 | union all
66 | (select valor, tipo, descricao, realizada_em
67 | from transacoes
68 | where cliente_id = ?
69 | order by id desc limit 10)"
70 | cliente_id cliente_id])
71 | saldo-row (first resultado)
72 | saldo {:total (:valor saldo-row)
73 | :data_extrato (:realizada_em saldo-row)
74 | :limite (:limite (get clientes cliente_id))}
75 | transacoes (rest resultado)]
76 | {:status 200
77 | :body {:saldo saldo
78 | :ultimas_transacoes transacoes}}))
79 |
80 | (defn ^:private transacionar!*
81 | [{db-spec :db-spec
82 | payload :body
83 | cliente_id :cliente-id
84 | clientes :cached-clientes}]
85 | (if-not (s/check payloads/Transacao payload)
86 | (let [{valor :valor
87 | tipo :tipo
88 | descricao :descricao} payload
89 | tx-fn {"d" debitar!
90 | "c" creditar!}]
91 | ((tx-fn tipo) cliente_id clientes valor descricao db-spec))
92 | {:status 422
93 | :body {:erro "manda essa merda direito com 'valor', 'tipo' e 'descricao'"}}))
94 |
95 | (defn ^:private transacionar-proc!*
96 | [{db-spec :db-spec
97 | payload :body
98 | cliente_id :cliente-id
99 | clientes :cached-clientes}]
100 | (if-not (s/check payloads/Transacao payload)
101 | (let [{limite :limite} (get clientes cliente_id)
102 | {valor :valor
103 | tipo :tipo
104 | descricao :descricao} payload
105 | proc {"d" "debitar"
106 | "c" "creditar"}
107 | {novo-saldo :novo_saldo
108 | possui-erro? :possui_erro
109 | mensagem :mensagem} (jdbc/execute-one!
110 | db-spec
111 | [(format "select novo_saldo, possui_erro, mensagem from %s(?, ?, ?)" (proc tipo))
112 | cliente_id
113 | valor
114 | descricao])]
115 | (if possui-erro?
116 | {:status 422
117 | :body {:erro mensagem}}
118 | {:status 200
119 | :body {:limite limite
120 | :saldo novo-saldo}}))
121 | {:status 422
122 | :body {:erro "manda essa merda direito com 'valor', 'tipo' e 'descricao'"}}))
123 |
124 | (defn ^:private transacionar-proc-tx!*
125 | "Life's too short to use transactions in the Rinha de Backend kkk"
126 | [{db-spec :db-spec
127 | payload :body
128 | cliente_id :cliente-id
129 | clientes :cached-clientes}]
130 | (if-not (s/check payloads/Transacao payload)
131 | (jdbc/with-transaction [conn db-spec]
132 | (let [{limite :limite} (get clientes cliente_id)
133 | {valor :valor
134 | tipo :tipo
135 | descricao :descricao} payload
136 | proc {"d" "debitar"
137 | "c" "creditar"}
138 | {novo-saldo :novo_saldo
139 | possui-erro? :possui_erro
140 | mensagem :mensagem} (jdbc/execute-one!
141 | conn
142 | [(format "select novo_saldo, possui_erro, mensagem from %s(?, ?, ?)" (proc tipo))
143 | cliente_id
144 | valor
145 | descricao])]
146 | (if possui-erro?
147 | {:status 422
148 | :body {:erro mensagem}}
149 | {:status 200
150 | :body {:limite limite
151 | :saldo novo-saldo}})))
152 | {:status 422
153 | :body {:erro "manda essa merda direito com 'valor', 'tipo' e 'descricao'"}}))
154 |
155 | (defn find-cliente-handler-wrapper
156 | [handler]
157 | (fn [{{cliente_id* :id} :route-params
158 | clientes :cached-clientes :as request}]
159 | (if-let [{cliente_id :id} (get clientes (Integer/parseInt cliente_id*))]
160 | (handler (assoc request :cliente-id cliente_id))
161 | {:status 404})))
162 |
163 | (def ^:private transaction-fn
164 | (if (= (System/getenv "API_USE_DB_FUNC") "true")
165 | transacionar-proc!*
166 | transacionar!*))
167 |
168 | (def transacionar! (find-cliente-handler-wrapper transaction-fn))
169 |
170 | (def extrato! (find-cliente-handler-wrapper extrato!*))
171 |
172 | (defn admin-reset-db!
173 | [{:keys [db-spec]}]
174 | (jdbc/execute-one! db-spec
175 | ["update saldos set valor = 0;
176 | truncate table transacoes;"])
177 | {:status 200
178 | :body {:msg "db reset!"}})
179 |
--------------------------------------------------------------------------------