Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frameworks/sinatra/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ source 'https://rubygems.org'
gem 'sinatra', '~> 4.1'
gem 'puma', '~> 8.0'
gem 'pg', '~> 1.5'
gem 'redis'
gem 'json'
gem 'concurrent-ruby'
gem 'connection_pool'
7 changes: 7 additions & 0 deletions frameworks/sinatra/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ GEM
rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
redis (5.4.1)
redis-client (>= 0.22.0)
redis-client (0.29.0)
connection_pool
sinatra (4.2.1)
logger (>= 1.6.0)
mustermann (~> 3.0)
Expand All @@ -39,6 +43,7 @@ DEPENDENCIES
json
pg (~> 1.5)
puma (~> 8.0)
redis
sinatra (~> 4.1)

CHECKSUMS
Expand All @@ -55,6 +60,8 @@ CHECKSUMS
rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac
rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8
redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
redis-client (0.29.0) sha256=0c65bf1f8f6dca22063ddb085c0bb2054feef6f03a84869f4161b18a9a15bea3
sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080
tilt (2.7.0) sha256=0d5b9ba69f6a36490c64b0eee9f6e9aad517e20dcc848800a06eb116f08c6ab3

Expand Down
154 changes: 140 additions & 14 deletions frameworks/sinatra/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,18 @@ class App < Sinatra::Base
set :public_folder, DATA_DIR
end

PG_QUERY = 'SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN $1 AND $2 LIMIT $3'.freeze
CRUD_COLUMNS = 'id, name, category, price, quantity, active, tags, rating_score, rating_count'
SELECT_QUERY = "SELECT #{CRUD_COLUMNS} FROM items WHERE price BETWEEN $1 AND $2 LIMIT $3".freeze
CRUD_GET_SQL = "SELECT #{CRUD_COLUMNS} FROM items WHERE id = $1 LIMIT 1"
CRUD_LIST_SQL = "SELECT #{CRUD_COLUMNS} FROM items WHERE category = $1 ORDER BY id LIMIT $2 OFFSET $3"
CRUD_UPDATE_SQL = "UPDATE items SET name = $1, price = $2, quantity = $3 WHERE id = $4"
CRUD_UPSERT_SQL = <<~SQL
INSERT INTO items
(#{CRUD_COLUMNS})
VALUES ($1, $2, $3, $4, $5, true, '[\"bench\"]', 0, 0)
ON CONFLICT (id) DO UPDATE SET name = $2, price = $4, quantity = $5
RETURNING id
SQL

get '/pipeline' do
render_plain 'ok'
Expand Down Expand Up @@ -109,20 +120,105 @@ class App < Sinatra::Base
end || []

items = rows.map do |row|
{
id: row[:id],
name: row[:name],
category: row[:category],
price: row[:price],
quantity: row[:quantity],
active: row[:active] == 1,
tags: JSON.parse(row[:tags]),
rating: { score: row[:rating_score], count: row[:rating_count] }
}
map_row(row)
end
render_json JSON.generate(items: items, count: items.length)
end

get '/crud/items' do
category = request.params['category'] || 'electronics'
page = (request.params['page'] || 1).to_i
limit = (request.params['limit'] || 10).to_i
offset = (page - 1) * limit

rows = self.class.get_async_db&.with do |connection|
connection.exec_prepared('crud_list', [category, limit, offset])
end || []

items = rows.map do |row|
map_row(row)
end
render_json JSON.generate({ items: items, total: items.length, page: page, limit: limit })
end

get '/crud/items/:id' do
id = params['id']
json = self.class.redis&.with do |connection|
connection.get(id.to_s)
end
if json
response['x-cache'] = 'HIT'
return render_json json
else
response['x-cache'] = 'MISS'
end

rows = self.class.get_async_db&.with do |connection|
connection.exec_prepared('crud_get', [id])
end || []

if row = rows.first
item = map_row(row)
json = JSON.generate(item)
self.class.redis&.with do |connection|
connection.set(id.to_s, json)
end
render_json json
else
not_found
end
end

post '/crud/items' do
params = JSON.parse(request.body.read)
id = params['id']
name = params['name'] || 'New Product'
category = params['category'] || 'electronics'
price = (params['price'] || 0).to_i
quantity = (params['quantity'] || 0).to_i

self.class.get_async_db&.with do |connection|
connection.exec_prepared('crud_upsert', [id, name, category, price, quantity])
end

self.class.redis&.with do |connection|
connection.del(id.to_s)
end

item = {
'id' => id,
'name' => name,
'category' => category,
'price' => price,
'quantity' => quantity
}

response.status = 201
render_json JSON.generate(item)
end

put '/crud/items/:id' do
id = params['id']
name = params['name'] || 'New Product'
price = (params['price'] || 0).to_i
quantity = (params['quantity'] || 0).to_i

row = self.class.get_async_db&.with do |connection|
connection.exec_prepared('crud_update', [name, price, quantity, id])
end || []

self.class.redis&.with do |connection|
connection.del(id.to_s)
end

item = {
'id' => id,
'name' => name,
'price' => price,
'quantity' => quantity
}
render_json JSON.generate(item)
end
private

def render_json(json)
Expand All @@ -135,16 +231,46 @@ def render_plain(text)
text
end

def map_row(row)
mapped_row = {
id: row[:id],
name: row[:name],
category: row[:category],
price: row[:price],
quantity: row[:quantity],
active: row[:active] == 1,
}
mapped_row[:tags] = JSON.parse(row[:tags]) if row[:tags]
mapped_row[:rating] = { score: row[:rating_score], count: row[:rating_count] } if row[:rating_score] && row[:rating_count]
mapped_row
end

def self.get_async_db
@async_db ||= begin
return unless ENV['DATABASE_URL']
max_connections = ENV.fetch('MAX_THREADS', 4).to_i + ENV.fetch("MAX_IO_THREADS", 10).to_i
ConnectionPool.new(size: max_connections, timeout: 5) do
ConnectionPool.new(size: pool_size, timeout: 5) do
db = PG.connect(ENV['DATABASE_URL'])
db.field_name_type = :symbol
db.prepare('select', PG_QUERY)
db.prepare('select', SELECT_QUERY)
db.prepare('crud_get', CRUD_GET_SQL)
db.prepare('crud_list', CRUD_LIST_SQL)
db.prepare('crud_update', CRUD_UPDATE_SQL)
db.prepare('crud_upsert', CRUD_UPSERT_SQL)
db
end
end
end

def self.redis
@redis ||= begin
return unless ENV['REDIS_URL']
ConnectionPool::Wrapper.new(size: pool_size, timeout: 10) do
Redis.new(url: ENV['REDIS_URL'])
end
end
end

def self.pool_size
ENV.fetch('MAX_THREADS', 4).to_i + ENV.fetch("MAX_IO_THREADS", 10).to_i
end
end
1 change: 1 addition & 0 deletions frameworks/sinatra/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"api-4",
"api-16",
"async-db",
"crud",
"static"
],
"maintainers": ["p8"]
Expand Down
20 changes: 20 additions & 0 deletions site/data/crud-4096.json
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,26 @@
"status_4xx": 0,
"status_5xx": 0
},
{
"framework": "sinatra",
"language": "Ruby",
"rps": 64585,
"avg_latency": "61.16ms",
"p99_latency": "82.00ms",
"cpu": "2974.9%",
"memory": "3.9GiB",
"connections": 4096,
"threads": 64,
"duration": "5s",
"pipeline": 1,
"bandwidth": "20.10MB/s",
"input_bw": "5.54MB/s",
"reconnects": 4070,
"status_2xx": 968783,
"status_3xx": 0,
"status_4xx": 0,
"status_5xx": 0
},
{
"framework": "trillium",
"language": "Rust",
Expand Down
78 changes: 78 additions & 0 deletions site/static/logs/crud/4096/sinatra.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
[1] Puma starting in cluster mode...
[1] * Puma version: 8.0.1 ("Into the Arena")
[1] * Ruby version: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +MN +PRISM [x86_64-linux]
[1] * Min threads: 3
[1] * Max threads: 3
[1] * Environment: production
[1] * Master PID: 1
[1] * Workers: 62
[1] * Restarts: (✔) hot (✖) phased (✖) refork
[1] * Preloading application
[1] * Listening on http://0.0.0.0:8080
[1] * Listening on ssl://0.0.0.0:8081?cert=/certs/server.crt&key=/certs/server.key
[1] Use Ctrl-C to stop
[1] ! WARNING: Detected `RUBY_MN_THREADS=1`
[1] ! This setting is known to cause performance regressions with Puma.
[1] ! Consider disabling this environment variable: https://github.com/puma/puma/issues/3720
[1] - Worker 0 (PID: 9) booted in 0.1s, phase: 0
[1] - Worker 1 (PID: 13) booted in 0.1s, phase: 0
[1] - Worker 2 (PID: 17) booted in 0.09s, phase: 0
[1] - Worker 3 (PID: 21) booted in 0.09s, phase: 0
[1] - Worker 4 (PID: 25) booted in 0.09s, phase: 0
[1] - Worker 5 (PID: 29) booted in 0.09s, phase: 0
[1] - Worker 6 (PID: 33) booted in 0.09s, phase: 0
[1] - Worker 7 (PID: 37) booted in 0.09s, phase: 0
[1] - Worker 8 (PID: 42) booted in 0.09s, phase: 0
[1] - Worker 9 (PID: 46) booted in 0.09s, phase: 0
[1] - Worker 10 (PID: 51) booted in 0.09s, phase: 0
[1] - Worker 11 (PID: 57) booted in 0.08s, phase: 0
[1] - Worker 12 (PID: 62) booted in 0.08s, phase: 0
[1] - Worker 13 (PID: 69) booted in 0.08s, phase: 0
[1] - Worker 14 (PID: 76) booted in 0.08s, phase: 0
[1] - Worker 15 (PID: 82) booted in 0.08s, phase: 0
[1] - Worker 16 (PID: 88) booted in 0.08s, phase: 0
[1] - Worker 17 (PID: 94) booted in 0.08s, phase: 0
[1] - Worker 18 (PID: 100) booted in 0.07s, phase: 0
[1] - Worker 19 (PID: 104) booted in 0.07s, phase: 0
[1] - Worker 20 (PID: 110) booted in 0.07s, phase: 0
[1] - Worker 21 (PID: 116) booted in 0.07s, phase: 0
[1] - Worker 22 (PID: 121) booted in 0.07s, phase: 0
[1] - Worker 23 (PID: 127) booted in 0.07s, phase: 0
[1] - Worker 24 (PID: 133) booted in 0.07s, phase: 0
[1] - Worker 25 (PID: 138) booted in 0.07s, phase: 0
[1] - Worker 26 (PID: 144) booted in 0.07s, phase: 0
[1] - Worker 27 (PID: 149) booted in 0.06s, phase: 0
[1] - Worker 28 (PID: 155) booted in 0.06s, phase: 0
[1] - Worker 29 (PID: 163) booted in 0.06s, phase: 0
[1] - Worker 30 (PID: 173) booted in 0.06s, phase: 0
[1] - Worker 31 (PID: 178) booted in 0.06s, phase: 0
[1] - Worker 32 (PID: 184) booted in 0.06s, phase: 0
[1] - Worker 33 (PID: 190) booted in 0.06s, phase: 0
[1] - Worker 34 (PID: 195) booted in 0.05s, phase: 0
[1] - Worker 35 (PID: 201) booted in 0.05s, phase: 0
[1] - Worker 36 (PID: 206) booted in 0.05s, phase: 0
[1] - Worker 37 (PID: 211) booted in 0.05s, phase: 0
[1] - Worker 38 (PID: 216) booted in 0.05s, phase: 0
[1] - Worker 39 (PID: 222) booted in 0.05s, phase: 0
[1] - Worker 40 (PID: 228) booted in 0.05s, phase: 0
[1] - Worker 41 (PID: 234) booted in 0.05s, phase: 0
[1] - Worker 42 (PID: 239) booted in 0.05s, phase: 0
[1] - Worker 43 (PID: 244) booted in 0.04s, phase: 0
[1] - Worker 44 (PID: 250) booted in 0.04s, phase: 0
[1] - Worker 45 (PID: 255) booted in 0.04s, phase: 0
[1] - Worker 46 (PID: 259) booted in 0.04s, phase: 0
[1] - Worker 47 (PID: 266) booted in 0.04s, phase: 0
[1] - Worker 48 (PID: 272) booted in 0.04s, phase: 0
[1] - Worker 49 (PID: 278) booted in 0.04s, phase: 0
[1] - Worker 50 (PID: 284) booted in 0.04s, phase: 0
[1] - Worker 51 (PID: 290) booted in 0.03s, phase: 0
[1] - Worker 52 (PID: 296) booted in 0.03s, phase: 0
[1] - Worker 53 (PID: 302) booted in 0.03s, phase: 0
[1] - Worker 54 (PID: 309) booted in 0.03s, phase: 0
[1] - Worker 55 (PID: 315) booted in 0.03s, phase: 0
[1] - Worker 56 (PID: 320) booted in 0.03s, phase: 0
[1] - Worker 57 (PID: 327) booted in 0.03s, phase: 0
[1] - Worker 58 (PID: 332) booted in 0.03s, phase: 0
[1] - Worker 59 (PID: 339) booted in 0.02s, phase: 0
[1] - Worker 60 (PID: 344) booted in 0.02s, phase: 0
[1] - Worker 61 (PID: 350) booted in 0.02s, phase: 0