├── 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 | logo nginx 5 |
6 | logo clojure 7 | logo postgres 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 | --------------------------------------------------------------------------------