├── .dockerignore ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── app ├── count.bash ├── create.bash ├── find.bash ├── handler.bash ├── not-found.bash └── search.bash ├── docker-compose-prod.yml ├── docker-compose.yml ├── init.sql ├── netcat.bash ├── nginx.conf └── views ├── 201.http ├── 400.http ├── 404.htmlr ├── 422.http ├── count.textr ├── find-not-found.jsonr ├── find.jsonr ├── search-no-results.jsonr └── search.jsonr /.dockerignore: -------------------------------------------------------------------------------- 1 | stress-test 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | stress-test 2 | response 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu AS base 2 | RUN apt update && apt install -y netcat postgresql-client socat 3 | WORKDIR /app 4 | 5 | FROM base AS prod 6 | COPY . . 7 | EXPOSE 3000 8 | CMD ["bash", "netcat.bash"] 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | .ONESHELL: 3 | .DEFAULT_GOAL: help 4 | 5 | help: ## Prints available commands 6 | @awk 'BEGIN {FS = ":.*##"; printf "Usage: make \033[36m\033[0m\n"} /^[.a-zA-Z_-]+:.*?##/ { printf " \033[36m%-25s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 7 | 8 | start.dev: ## Start the rinha in Dev 9 | @docker-compose up -d nginx 10 | 11 | start.prod: ## Start the rinha in Prod 12 | @docker-compose -f docker-compose-prod.yml up -d nginx 13 | 14 | docker.stats: ## Show docker stats 15 | @docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}" 16 | 17 | health.check: ## Check the stack is healthy 18 | @curl -v http://localhost:9999/contagem-pessoas 19 | 20 | stress.it: ## Run stress tests 21 | @sh stress-test/run-test.sh 22 | 23 | docker.build: ## Build the docker image 24 | @docker build -t leandronsp/rinha-backend-bash --target prod . 25 | 26 | docker.push: ## Push the docker image 27 | @docker push leandronsp/rinha-backend-bash 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rinha-backend-bash 2 | 3 | Versão Bash da [rinha do backend](https://github.com/zanfranceschi/rinha-de-backend-2023-q3) 4 | 5 | ## Requisitos 6 | 7 | * Bash 8 | * [Docker](https://docs.docker.com/get-docker/) 9 | * [curl](https://curl.se/download.html) 10 | * [Gatling](https://gatling.io/open-source/), a performance testing tool 11 | * _Maizena_ 12 | 13 | ## Usage 14 | 15 | ```bash 16 | $ make help 17 | 18 | Usage: make 19 | help Prints available commands 20 | start.dev Start the rinha in Dev 21 | start.prod Start the rinha in Prod 22 | docker.stats Show docker stats 23 | health.check Check the stack is healthy 24 | stress.it Run stress tests 25 | docker.build Build the docker image 26 | docker.push Push the docker image 27 | ``` 28 | 29 | ## Stack 30 | 31 | * Bash 32 | * PostgreSQL 33 | * NGINX 34 | -------------------------------------------------------------------------------- /app/count.bash: -------------------------------------------------------------------------------- 1 | function handle_GET_count() { 2 | RESULT=`psql -t -h pgbouncer -U postgres -d postgres -p 6432 -c "SELECT COUNT(*) FROM people"` 3 | RESPONSE=$(cat views/count.textr | sed "s/{{count}}/$RESULT/") 4 | } 5 | -------------------------------------------------------------------------------- /app/create.bash: -------------------------------------------------------------------------------- 1 | function handle_POST_create() { 2 | if [ ! -z "$BODY" ]; then 3 | UUID=$(cat /proc/sys/kernel/random/uuid) 4 | 5 | QUERY=" 6 | WITH data AS ( 7 | SELECT 8 | '$BODY'::json AS item 9 | ) 10 | INSERT INTO people (id, name, nickname, birth_date, stack) 11 | SELECT 12 | '$UUID', 13 | item->>'nome', 14 | item->>'apelido', 15 | to_date(item->>'nascimento', 'YYYY-MM-DD'), 16 | ARRAY[item->>'stack'] 17 | FROM data" 18 | 19 | psql -h pgbouncer -U postgres -d postgres -p 6432 -c "$QUERY" >&2 20 | PSQL_STATUS=$? 21 | 22 | if [ $PSQL_STATUS -ne 0 ]; then 23 | RESPONSE=$(cat views/422.http) 24 | else 25 | RESPONSE=$(cat views/201.http | sed "s/{{uuid}}/$UUID/") 26 | fi 27 | 28 | fi 29 | } 30 | -------------------------------------------------------------------------------- /app/find.bash: -------------------------------------------------------------------------------- 1 | function handle_GET_find() { 2 | UUID=${PARAMS["id"]} 3 | 4 | if [ ! -z "$UUID" ]; then 5 | QUERY=" 6 | SELECT json_agg(row_to_json(t)) 7 | FROM ( 8 | SELECT 9 | id, 10 | name as nome, 11 | nickname as apelido, 12 | birth_date as nascimento, 13 | stack 14 | FROM people 15 | WHERE id = '$UUID' 16 | ) t" 17 | 18 | RESULT=`psql -t -h pgbouncer -U postgres -d postgres -p 6432 -c "$QUERY" | tr -d '[:space:]'` 19 | 20 | if [ ! -z "$RESULT" ]; then 21 | RESPONSE=$(cat views/find.jsonr | sed "s/{{data}}/$RESULT/") 22 | else 23 | RESPONSE=$(cat views/find-not-found.jsonr) 24 | fi 25 | fi 26 | } 27 | -------------------------------------------------------------------------------- /app/handler.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | declare -A params 4 | 5 | function handleRequest() { 6 | ## Read the HTTP request until \r\n 7 | while read line; do 8 | #echo $line 9 | trline=$(echo $line | tr -d '[\r\n]') ## Removes the \r\n from the EOL 10 | 11 | ## Breaks the loop when line is empty 12 | [ -z "$trline" ] && break 13 | 14 | ## Parses the headline 15 | ## e.g GET /contagem-pessoas HTTP/1.1 -> GET /contagem-pessoas 16 | HEADLINE_REGEX='(.*?)\s(.*?)\sHTTP.*?' 17 | 18 | if [[ "$trline" =~ $HEADLINE_REGEX ]]; then 19 | REQUEST=$(echo $trline | sed -E "s/$HEADLINE_REGEX/\1 \2/") 20 | echo $REQUEST >&2 21 | 22 | ## Parses the query string 23 | QUERY_STRING_REGEX='(.*?)\?t=(.*)' 24 | if [[ "$REQUEST" =~ $QUERY_STRING_REGEX ]]; then 25 | PARAMS["term"]=$(echo $REQUEST | sed -E "s/$QUERY_STRING_REGEX/\2/") 26 | REQUEST=$(echo $REQUEST | sed -E "s/$QUERY_STRING_REGEX/\1/") 27 | fi 28 | 29 | ## Parses the path parameter (UUID) 30 | # e.g GET /pessoas/123e4567 HTTP/1.1 -> GET /pessoas/:id -> 123e4567 31 | PATH_PARAMETER_REGEX='(.*?\s\/.*?)\/(.*?)$' 32 | if [[ "$REQUEST" =~ $PATH_PARAMETER_REGEX ]]; then 33 | PARAMS["id"]=$(echo $REQUEST | sed -E "s/$PATH_PARAMETER_REGEX/\2/") 34 | REQUEST=$(echo $REQUEST | sed -E "s/$PATH_PARAMETER_REGEX/\1\/:id/") 35 | fi 36 | fi 37 | 38 | ## Parses the Content-Length header 39 | ## e.g Content-Length: 42 -> 42 40 | CONTENT_LENGTH_REGEX='Content-Length:\s(.*?)' 41 | [[ "$trline" =~ $CONTENT_LENGTH_REGEX ]] && 42 | CONTENT_LENGTH=$(echo $trline | sed -E "s/$CONTENT_LENGTH_REGEX/\1/") 43 | 44 | ## Parses the Cookie header 45 | ## e.g Cookie: name=John -> name John 46 | COOKIE_REGEX='Cookie:\s(.*?)\=(.*?).*?' 47 | [[ "$trline" =~ $COOKIE_REGEX ]] && 48 | read COOKIE_NAME COOKIE_VALUE <<< $(echo $trline | sed -E "s/$COOKIE_REGEX/\1 \2/") 49 | done 50 | 51 | ## Read the remaining HTTP request body 52 | if [ ! -z "$CONTENT_LENGTH" ]; then 53 | read -n$CONTENT_LENGTH BODY 54 | fi 55 | 56 | ## Route request to the response handler 57 | source ./app/search.bash 58 | source ./app/count.bash 59 | source ./app/find.bash 60 | source ./app/create.bash 61 | source ./app/not-found.bash 62 | 63 | ## Route request to the response handler 64 | case "$REQUEST" in 65 | "GET /contagem-pessoas") handle_GET_count ;; 66 | "GET /pessoas") handle_GET_search ;; 67 | "GET /pessoas/:id") handle_GET_find ;; 68 | "POST /pessoas") handle_POST_create ;; 69 | *) handle_not_found ;; 70 | esac 71 | 72 | echo -e "$RESPONSE" > $FIFO_PATH 73 | } 74 | -------------------------------------------------------------------------------- /app/not-found.bash: -------------------------------------------------------------------------------- 1 | function handle_not_found() { 2 | RESPONSE=$(cat views/404.htmlr) 3 | } 4 | -------------------------------------------------------------------------------- /app/search.bash: -------------------------------------------------------------------------------- 1 | function handle_GET_search() { 2 | SEARCH_TERM=${PARAMS["term"]} 3 | 4 | if [ -z "$SEARCH_TERM" ]; then 5 | RESPONSE=$(cat views/400.http) 6 | return 7 | fi 8 | 9 | if [ ! -z "$SEARCH_TERM" ]; then 10 | QUERY=" 11 | SELECT json_agg(row_to_json(t)) 12 | FROM ( 13 | SELECT 14 | id, 15 | name as nome, 16 | nickname as apelido, 17 | birth_date as nascimento, 18 | stack 19 | FROM people 20 | WHERE search LIKE '%$SEARCH_TERM%' 21 | LIMIT 50 22 | ) t" 23 | 24 | RESULT=`psql -t -h pgbouncer -U postgres -d postgres -p 6432 -c "$QUERY" | tr -d '[:space:]'` 25 | 26 | if [ ! -z "$RESULT" ]; then 27 | RESPONSE=$(cat views/search.jsonr | sed "s/{{data}}/$RESULT/") 28 | else 29 | RESPONSE=$(cat views/search-no-results.jsonr) 30 | fi 31 | fi 32 | } 33 | -------------------------------------------------------------------------------- /docker-compose-prod.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | api1: &api 5 | image: leandronsp/rinha-backend-bash 6 | container_name: api1 7 | depends_on: 8 | - postgres 9 | deploy: 10 | resources: 11 | limits: 12 | cpus: '0.5' 13 | memory: '0.5GB' 14 | 15 | api2: 16 | <<: *api 17 | container_name: api2 18 | 19 | postgres: 20 | image: postgres 21 | container_name: postgres 22 | ports: 23 | - 5432:5432 24 | environment: 25 | - POSTGRES_HOST_AUTH_METHOD=trust 26 | volumes: 27 | - ./init.sql:/docker-entrypoint-initdb.d/init.sql 28 | deploy: 29 | resources: 30 | limits: 31 | cpus: '0.8' 32 | memory: '0.5GB' 33 | 34 | pgbouncer: 35 | image: pgbouncer/pgbouncer 36 | hostname: pgbouncer 37 | container_name: pgbouncer 38 | environment: 39 | - DATABASES_HOST=postgres 40 | - DATABASES_PORT=5432 41 | - DATABASES_USER=postgres 42 | - DATABASES_PASSWORD=postgres 43 | - DATABASES_DBNAME=postgres 44 | - DATABASES_POOL_SIZE=100 45 | - PGBOUNCER_MAX_CLIENT_CONN=100 46 | - PGBOUNCER_RESERVE_POOL_SIZE=100 47 | - PGBOUNCER_DEFAULT_POOL_SIZE=100 48 | depends_on: 49 | - postgres 50 | deploy: 51 | resources: 52 | limits: 53 | cpus: '0.5' 54 | memory: '0.5GB' 55 | 56 | nginx: 57 | image: nginx 58 | container_name: nginx 59 | volumes: 60 | - ./nginx.conf:/etc/nginx/nginx.conf:ro 61 | depends_on: 62 | - api1 63 | - api2 64 | ports: 65 | - 9999:9999 66 | deploy: 67 | resources: 68 | limits: 69 | cpus: '0.2' 70 | memory: '0.3GB' 71 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | api1: &api 5 | build: 6 | context: . 7 | target: base 8 | container_name: api1 9 | depends_on: 10 | - postgres 11 | - pgbouncer 12 | volumes: 13 | - .:/app 14 | command: bash netcat.bash 15 | deploy: 16 | resources: 17 | limits: 18 | cpus: '0.25' 19 | memory: '0.5GB' 20 | 21 | api2: 22 | <<: *api 23 | container_name: api2 24 | 25 | postgres: 26 | image: postgres 27 | container_name: postgres 28 | ports: 29 | - 5432:5432 30 | environment: 31 | - POSTGRES_HOST_AUTH_METHOD=trust 32 | volumes: 33 | - ./init.sql:/docker-entrypoint-initdb.d/init.sql 34 | deploy: 35 | resources: 36 | limits: 37 | cpus: '0.8' 38 | memory: '1GB' 39 | 40 | pgbouncer: 41 | image: pgbouncer/pgbouncer 42 | hostname: pgbouncer 43 | container_name: pgbouncer 44 | environment: 45 | - DATABASES_HOST=postgres 46 | - DATABASES_PORT=5432 47 | - DATABASES_USER=postgres 48 | - DATABASES_PASSWORD=postgres 49 | - DATABASES_DBNAME=postgres 50 | - DATABASES_POOL_SIZE=100 51 | - PGBOUNCER_MAX_CLIENT_CONN=100 52 | - PGBOUNCER_RESERVE_POOL_SIZE=100 53 | - PGBOUNCER_DEFAULT_POOL_SIZE=100 54 | depends_on: 55 | - postgres 56 | deploy: 57 | resources: 58 | limits: 59 | cpus: '0.1' 60 | memory: '0.3GB' 61 | 62 | nginx: 63 | image: nginx 64 | container_name: nginx 65 | volumes: 66 | - ./nginx.conf:/etc/nginx/nginx.conf:ro 67 | depends_on: 68 | - api1 69 | - api2 70 | ports: 71 | - 9999:9999 72 | deploy: 73 | resources: 74 | limits: 75 | cpus: '0.1' 76 | memory: '0.3GB' 77 | -------------------------------------------------------------------------------- /init.sql: -------------------------------------------------------------------------------- 1 | -- Create extensions 2 | CREATE EXTENSION IF NOT EXISTS pg_trgm; 3 | 4 | -- Create table people 5 | CREATE TABLE IF NOT EXISTS people ( 6 | id UUID PRIMARY KEY, 7 | nickname VARCHAR(32) UNIQUE NOT NULL, 8 | name VARCHAR(100) NOT NULL, 9 | birth_date DATE NOT NULL, 10 | stack VARCHAR(1024), 11 | search VARCHAR(1160) GENERATED ALWAYS AS ( 12 | LOWER(name) || ' ' || LOWER(nickname) || ' ' || LOWER(stack) 13 | ) STORED 14 | ); 15 | 16 | -- Create search index 17 | CREATE INDEX CONCURRENTLY people_search_idx ON people USING GIN (search gin_trgm_ops); 18 | -------------------------------------------------------------------------------- /netcat.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PID=$$ 4 | 5 | ## Create the response FIFO 6 | mkdir -p /tmp/pid-$PID 7 | FIFO_PATH=/tmp/pid-$PID/response 8 | rm -f $FIFO_PATH 9 | mkfifo $FIFO_PATH 10 | 11 | source ./app/handler.bash 12 | 13 | echo 'Listening on 3000...' 14 | 15 | ## Keep the server running forever 16 | while true; do 17 | ## 1. wait for FIFO 18 | ## 2. creates a socket and listens to the port 3000 19 | ## 3. as soon as a request message arrives to the socket, pipes it to the handleRequest function 20 | ## 4. the handleRequest function processes the request message and routes it to the response handler, which writes to the FIFO 21 | ## 5. as soon as the FIFO receives a message, it's sent to the socket 22 | ## 6. closes the connection (`-N`), closes the socket and repeat the loop 23 | cat $FIFO_PATH | nc -lN 3000 | handleRequest 24 | done 25 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes auto; 2 | 3 | events { 4 | worker_connections 256; 5 | } 6 | 7 | http { 8 | upstream api { 9 | server api1:3000; 10 | server api2:3000; 11 | } 12 | 13 | server { 14 | listen 9999; 15 | 16 | location / { 17 | proxy_pass http://api; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /views/201.http: -------------------------------------------------------------------------------- 1 | HTTP/1.1 201 Created 2 | Location: /pessoas/{{uuid}} 3 | 4 | "" 5 | -------------------------------------------------------------------------------- /views/400.http: -------------------------------------------------------------------------------- 1 | HTTP/1.1 400 Bad Request 2 | 3 | "" 4 | -------------------------------------------------------------------------------- /views/404.htmlr: -------------------------------------------------------------------------------- 1 | HTTP/1.1 404 NotFound 2 | Content-Type: text/html 3 | 4 |

Sorry, not found

5 | -------------------------------------------------------------------------------- /views/422.http: -------------------------------------------------------------------------------- 1 | HTTP/1.1 422 Unprocessable Content 2 | 3 | "" 4 | -------------------------------------------------------------------------------- /views/count.textr: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 2 | Content-Type: text/plain 3 | 4 | {{count}} 5 | -------------------------------------------------------------------------------- /views/find-not-found.jsonr: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 2 | Content-Type: application/json 3 | 4 | {} 5 | -------------------------------------------------------------------------------- /views/find.jsonr: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 2 | Content-Type: application/json 3 | 4 | {{data}} 5 | -------------------------------------------------------------------------------- /views/search-no-results.jsonr: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 2 | Content-Type: application/json 3 | 4 | [] 5 | -------------------------------------------------------------------------------- /views/search.jsonr: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 2 | Content-Type: application/json 3 | 4 | {{data}} 5 | --------------------------------------------------------------------------------