├── .gitignore ├── tests ├── nim.cfg └── tdatabase.nim ├── README.md ├── rinhabackend_nim_jester.nimble ├── postgresql.conf ├── nginx.conf ├── Dockerfile ├── docker-compose.yml └── src ├── rinha.nim ├── types.nim └── database.nim /.gitignore: -------------------------------------------------------------------------------- 1 | rinha 2 | -------------------------------------------------------------------------------- /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | --path:"../src/" 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rinha Backend API in Nim/Jester 2 | 3 | Attempting to a simple Nim web app 4 | 5 | nimble install -y 6 | nimble build 7 | ./main 8 | 9 | Or, directly: 10 | 11 | nimble run 12 | 13 | Run tests: 14 | 15 | nimble test 16 | -------------------------------------------------------------------------------- /rinhabackend_nim_jester.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.0" 4 | author = "AkitaOnRails" 5 | description = "Rinha Backend API" 6 | license = "MIT" 7 | srcDir = "src" 8 | bin = @["rinha"] 9 | 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 1.6.14" 14 | requires "jester" 15 | requires "pg" 16 | requires "print" 17 | requires "pg" 18 | requires "uuids" 19 | requires "fusion" 20 | -------------------------------------------------------------------------------- /postgresql.conf: -------------------------------------------------------------------------------- 1 | listen_addresses = '*' 2 | max_connections = 500 3 | superuser_reserved_connections = 3 4 | unix_socket_directories = '/var/run/postgresql' 5 | shared_buffers = 512MB 6 | work_mem = 4MB 7 | maintenance_work_mem = 256MB 8 | effective_cache_size = 1GB 9 | wal_buffers = 64MB 10 | checkpoint_timeout = 10min 11 | checkpoint_completion_target = 0.9 12 | random_page_cost = 4.0 13 | effective_io_concurrency = 2 14 | autovacuum = on 15 | log_statement = 'none' 16 | log_duration = off 17 | log_lock_waits = on 18 | log_error_verbosity = terse 19 | log_min_messages = fatal 20 | log_min_error_statement = fatal 21 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes auto; 2 | worker_rlimit_nofile 500000; 3 | 4 | events { 5 | use epoll; 6 | worker_connections 512; 7 | } 8 | 9 | http { 10 | access_log off; 11 | error_log /dev/null emerg; 12 | 13 | upstream api { 14 | server localhost:3000; 15 | server localhost:3001; 16 | keepalive 500; 17 | } 18 | server { 19 | listen 9999; 20 | 21 | location / { 22 | proxy_buffering off; 23 | proxy_set_header Connection ""; 24 | proxy_http_version 1.1; 25 | proxy_set_header Keep-Alive ""; 26 | proxy_set_header Proxy-Connection "keep-alive"; 27 | proxy_pass http://api; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Nim image from Docker Hub 2 | FROM nimlang/nim:latest AS build-env 3 | 4 | # Install PostgreSQL client library 5 | RUN apt-get update && apt-get install -y libpq-dev 6 | 7 | # Set the working directory 8 | WORKDIR /app 9 | 10 | COPY rinhabackend_nim_jester.nimble rinhabackend_nim_jester.nimble 11 | 12 | # Install any Nimble dependencies 13 | RUN nimble install -y -d 14 | 15 | # Copy the current directory contents into the container 16 | COPY . . 17 | 18 | # Compile the Nim application 19 | # source: https://github.com/dom96/httpbeast/issues/19 20 | # RUN nimble compile --threads:on --opt:speed -d:release src/rinha.nim -o:/app/rinha 21 | RUN nimble compile --opt:speed -d:release src/rinha.nim -o:/app/rinha 22 | 23 | EXPOSE 3000 24 | 25 | # Run the compiled Nim application 26 | CMD ["sh", "-c", "/app/rinha"] 27 | -------------------------------------------------------------------------------- /tests/tdatabase.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch, uuids, times 2 | import std/[options,json] 3 | import database, types 4 | import unittest 5 | {.experimental: "caseStmtMacros".} 6 | 7 | import fusion/matching 8 | suite "database testing": 9 | setup: 10 | initDb() 11 | waitFor createPessoaTable() 12 | 13 | test "simple scenario": 14 | let uuid = $genUUID() 15 | let pessoa = Pessoa( 16 | id: some(uuid), 17 | apelido: "foo", 18 | nome: some("nome"), 19 | nascimento: some("2000-01-01"), 20 | stack: some(@["foo", "bar", "baz"])) 21 | 22 | try: 23 | waitFor insertPessoa(pessoa) 24 | except: 25 | raise newException(IOError, "I can't do that Dave.") 26 | 27 | let total = waitFor getPessoasCount() 28 | check(total == 1) 29 | 30 | let res = waitFor getPessoaById(uuid) 31 | case res 32 | of Some(@pessoa): 33 | check(pessoa.id.get() == uuid) 34 | of None(): 35 | raise newException(IOError, "I can't do that Dave.") 36 | 37 | let results = waitFor searchPessoas("foo") 38 | check(len(results) == 1) 39 | 40 | test "json parsing": 41 | let body = """ 42 | { 43 | "apelido": "jose", 44 | "nome": "Jose Roberto", 45 | "nascimento": "2000-02-01", 46 | "stack": ["foo", "bar", "baz"] 47 | } 48 | """ 49 | let json = parseJson(body) 50 | 51 | let pessoa = to(json, Pessoa) 52 | check(pessoa.apelido == "jose") 53 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | api1: &api 4 | #image: docker.io/akitaonrails/rinhabackendapi-nim-jester:latest 5 | build: . 6 | environment: 7 | - DB_HOST=localhost 8 | - DB_POOL_SIZE=20 9 | - OS_NUM_THREADS=4 10 | - PORT=3000 11 | depends_on: 12 | - postgres 13 | network_mode: host 14 | deploy: 15 | resources: 16 | limits: 17 | cpus: '0.55' 18 | memory: '0.4GB' 19 | 20 | api2: 21 | <<: *api 22 | environment: 23 | - DB_HOST=localhost 24 | - DB_POOL_SIZE=20 25 | - OS_NUM_THREADS=4 26 | - PORT=3001 27 | 28 | nginx: # Load Balancer 29 | image: docker.io/nginx:latest 30 | command: ["nginx", "-g", "daemon off;"] 31 | volumes: 32 | - ./nginx.conf:/etc/nginx/nginx.conf:ro 33 | depends_on: 34 | - api1 35 | - api2 36 | ulimits: 37 | nproc: 1000000 38 | nofile: 39 | soft: 1000000 40 | hard: 1000000 41 | network_mode: host 42 | deploy: 43 | resources: 44 | limits: 45 | cpus: '0.2' 46 | memory: '0.3GB' 47 | 48 | postgres: # Banco de dados 49 | image: docker.io/postgres 50 | hostname: postgres 51 | environment: 52 | POSTGRES_USERNAME: postgres 53 | POSTGRES_PASSWORD: password 54 | command: postgres -c 'max_connections=450' 55 | network_mode: host 56 | volumes: 57 | - ./postgresql.conf:/docker-entrypoint-initdb.d/postgresql.conf 58 | healthcheck: 59 | test: ["CMD-SHELL", "pg_isready"] 60 | interval: 5s 61 | timeout: 5s 62 | retries: 20 63 | start_period: 10s 64 | deploy: 65 | resources: 66 | limits: 67 | cpus: '0.2' 68 | memory: '1.9GB' 69 | -------------------------------------------------------------------------------- /src/rinha.nim: -------------------------------------------------------------------------------- 1 | # This is just an example to get you started. A typical binary package 2 | # uses this file as the main entry point of the application. 3 | import asyncdispatch, jester, strutils, sequtils, uuids, os 4 | import std/json 5 | import database, types 6 | 7 | router rinha_api: 8 | get "/contagem-pessoas": 9 | let count = await getPessoasCount() 10 | resp $count 11 | 12 | get "/pessoas/@pessoa_id": 13 | let res = await getPessoaById(@"pessoa_id") 14 | if isSome(res): 15 | resp(Http200, $toJson(res.get())) 16 | else: 17 | resp(Http404, "") 18 | 19 | get "/pessoas": 20 | if request.params.hasKey("t"): 21 | let results = await searchPessoas(request.params["t"]) 22 | resp(Http200, $toJson(results)) 23 | else: 24 | resp(Http400, "") 25 | 26 | post "/pessoas": 27 | try: 28 | let data = parseJson(request.body) 29 | let pessoa = to(data, Pessoa) 30 | 31 | let uuid = $genUUID() 32 | pessoa.id = some(uuid) 33 | if isNone(pessoa.stack): 34 | pessoa.stack = some(newSeq[string](0)) 35 | 36 | if all(pessoa.stack.get(), proc (x: string): bool = x.len <= 32): 37 | await insertPessoa(pessoa) 38 | setHeader(responseHeaders, "Location", "/pessoas/" & uuid) 39 | resp(Http201, "") 40 | else: 41 | resp(Http400, "") 42 | except: 43 | resp(Http422, "") 44 | 45 | proc main() = 46 | let port = parseInt(getEnv("PORT")) 47 | let settings = newSettings(port=Port(port)) 48 | var jester = initJester(rinha_api, settings=settings) 49 | 50 | echo "initializing database" 51 | initDb() 52 | try: 53 | echo "creating table" 54 | waitFor createPessoaTable() 55 | except: 56 | echo "the other node already created everything" 57 | 58 | jester.serve() 59 | 60 | when isMainModule: 61 | main() 62 | -------------------------------------------------------------------------------- /src/types.nim: -------------------------------------------------------------------------------- 1 | import std/[options,json] 2 | import os, strutils 3 | 4 | # Config definition 5 | type 6 | PgConfig* = object 7 | hostname*: string 8 | port*: int 9 | username*: string 10 | password*: string 11 | database*: string 12 | poolSize*: int 13 | 14 | proc parseEnv*() : PgConfig = 15 | var config = PgConfig() 16 | let hostname = getEnv("DB_HOST") 17 | if len(hostname) == 0: 18 | config.hostname = "localhost" 19 | else: 20 | config.hostname = hostname 21 | 22 | try: 23 | let port = parseInt(getEnv("DB_PORT")) 24 | config.port = port 25 | except: 26 | config.port = 5432 27 | 28 | let username = getEnv("DB_USERNAME") 29 | if len(username) == 0: 30 | config.username = "postgres" 31 | else: 32 | config.username = username 33 | 34 | let password = getEnv("DB_PASSWORD") 35 | if len(password) == 0: 36 | config.password = "password" 37 | else: 38 | config.password = password 39 | 40 | let database = getEnv("DB_DATABASE") 41 | if len(database) == 0: 42 | config.database = "postgres" 43 | else: 44 | config.database = database 45 | 46 | try: 47 | let poolSize = parseInt(getEnv("DB_POOL_SIZE")) 48 | config.poolSize = poolSize 49 | except: 50 | config.poolSize = 10 51 | 52 | return config 53 | 54 | # Model definition 55 | type 56 | Pessoa* = ref object of RootObj 57 | id*: Option[string] 58 | apelido*: string 59 | nome*: Option[string] 60 | nascimento*: Option[string] 61 | stack*: Option[seq[string]] 62 | 63 | # serializers 64 | proc toJson*(p: Pessoa): JsonNode = 65 | result = newJObject() 66 | result["id"] = %p.id 67 | result["apelido"] = %p.apelido 68 | result["nome"] = %p.nome 69 | result["nascimento"] = %p.nascimento 70 | result["stack"] = %p.stack 71 | 72 | proc toJson*(p: seq[Pessoa]): JsonNode = 73 | result = newJArray() 74 | for pessoa in p: 75 | result.add(pessoa.toJson()) 76 | 77 | proc fromJson*(node: JsonNode, T: typedesc[Option[string]]): Option[string] = 78 | if node.kind == JString: 79 | return some(node.getStr) 80 | else: 81 | return none(string) 82 | 83 | proc fromJson*(node: JsonNode, T: typedesc[Option[seq[string]]]): Option[seq[string]] = 84 | if node.kind == JArray: 85 | var arr: seq[string] = @[] 86 | for child in node: 87 | if child.kind == JString: 88 | arr.add(child.getStr) 89 | return some(arr) 90 | else: 91 | return none(seq[string]) 92 | 93 | proc fromJson*(node: JsonNode, T: typedesc[Pessoa]): Pessoa = 94 | new(result) 95 | for key, value in fieldpairs(result): 96 | node[key] = value 97 | -------------------------------------------------------------------------------- /src/database.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch, strutils, sequtils, strformat, os 2 | import std/options 3 | import pg 4 | import std/json 5 | import types 6 | 7 | # Initialize Database 8 | var global_pool* {.threadvar.}: AsyncPool 9 | 10 | proc initDb*() = 11 | let pgconf = parseEnv() 12 | 13 | var retries = 0 14 | while retries < 4: 15 | try: 16 | global_pool = newAsyncPool(pgconf.hostname, pgconf.username, pgconf.password, pgconf.database, pgconf.poolSize) 17 | return 18 | except: 19 | sleep(5000) 20 | inc(retries) 21 | 22 | quit(0) 23 | 24 | proc getGlobalPool(): AsyncPool = 25 | if global_pool.isNil: 26 | initDb() 27 | return global_pool 28 | 29 | proc createPessoaTable*() {.async.} = 30 | let sqlQueries = sql""" 31 | CREATE EXTENSION IF NOT EXISTS pg_trgm; 32 | 33 | DROP TABLE IF EXISTS public.pessoas CASCADE; 34 | CREATE TABLE public.pessoas ( 35 | id uuid DEFAULT gen_random_uuid() PRIMARY KEY, 36 | apelido character varying(32) NOT NULL, 37 | nome character varying(100) NOT NULL, 38 | nascimento timestamp(6) without time zone, 39 | stack character varying, 40 | searchable text GENERATED ALWAYS AS ((((((nome)::text || ' '::text) || (apelido)::text) || ' '::text) || (COALESCE(stack, ' '::character varying))::text)) STORED 41 | ); 42 | 43 | CREATE INDEX IF NOT EXISTS index_pessoas_on_id ON public.pessoas (id); 44 | 45 | CREATE UNIQUE INDEX IF NOT EXISTS index_pessoas_on_apelido ON public.pessoas USING btree (apelido); 46 | 47 | CREATE INDEX IF NOT EXISTS index_pessoas_on_searchable ON public.pessoas USING gist (searchable public.gist_trgm_ops); 48 | """ 49 | await getGlobalPool().exec(sqlQueries, @[]) 50 | 51 | # Insert a new Pessoa 52 | proc insertPessoa*(pessoa: Pessoa) {.async.} = 53 | let query = sql""" 54 | INSERT INTO public.pessoas (id, apelido, nome, nascimento, stack) 55 | VALUES (?, ?, ?, ?::timestamp(6), ?); 56 | """ 57 | await getGlobalPool().exec(query, @[pessoa.id.get(), pessoa.apelido, pessoa.nome.get(), pessoa.nascimento.get(), $(%*(pessoa.stack.get()))]) 58 | 59 | # Get the count of Pessoas 60 | proc getPessoasCount*(): Future[int] {.async.} = 61 | let query = sql""" 62 | SELECT COUNT(*) FROM public.pessoas; 63 | """ 64 | let result = await getGlobalPool().rows(query, @[]) 65 | return parseInt(result[0][0]) 66 | 67 | # Get Pessoa by ID 68 | proc getPessoaById*(id: string): Future[Option[Pessoa]] {.async.} = 69 | let query = sql""" 70 | SELECT id::text, apelido, nome, to_char(nascimento, 'YYYY-MM-DD'), stack 71 | FROM public.pessoas 72 | WHERE id = ?; 73 | """ 74 | try: 75 | let result = await getGlobalPool().rows(query, @[id]) 76 | if len(result) == 0: 77 | return none(Pessoa) 78 | 79 | let row = result[0] 80 | return some(Pessoa( 81 | id: some(row[0]), 82 | apelido: row[1], 83 | nome: some(row[2]), 84 | nascimento: some(row[3]), 85 | stack: some(row[4].split(",")))) 86 | except: 87 | return none(Pessoa) 88 | 89 | # Search Pessoas based on term 90 | proc searchPessoas*(term: string): Future[seq[Pessoa]] {.async.} = 91 | let query = sql""" 92 | SELECT id::text, apelido, nome, to_char(nascimento, 'YYYY-MM-DD'), stack 93 | FROM public.pessoas 94 | WHERE searchable ILIKE ?; 95 | """ 96 | let result = await getGlobalPool().rows(query, @["%" & term & "%"]) 97 | if len(result) == 0: 98 | return @[] 99 | 100 | return result.mapIt(Pessoa( 101 | id: some(it[0]), 102 | apelido: it[1], 103 | nome: some(it[2]), 104 | nascimento: some(it[3]), 105 | stack: some(it[4].split(",")) 106 | )) 107 | --------------------------------------------------------------------------------