From 23e6a577e35e4b59f6e5014dd7dca01e2dc5368e Mon Sep 17 00:00:00 2001 From: Petrik Date: Wed, 3 Jun 2026 17:56:34 +0200 Subject: [PATCH 1/2] [sinatra] Add crud --- frameworks/sinatra/Gemfile | 1 + frameworks/sinatra/Gemfile.lock | 7 ++ frameworks/sinatra/app.rb | 154 +++++++++++++++++++++++++++++--- frameworks/sinatra/meta.json | 1 + 4 files changed, 149 insertions(+), 14 deletions(-) diff --git a/frameworks/sinatra/Gemfile b/frameworks/sinatra/Gemfile index e81e0b888..38dcf1e11 100644 --- a/frameworks/sinatra/Gemfile +++ b/frameworks/sinatra/Gemfile @@ -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' diff --git a/frameworks/sinatra/Gemfile.lock b/frameworks/sinatra/Gemfile.lock index 3c95bb5a5..ebebd44b0 100644 --- a/frameworks/sinatra/Gemfile.lock +++ b/frameworks/sinatra/Gemfile.lock @@ -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) @@ -39,6 +43,7 @@ DEPENDENCIES json pg (~> 1.5) puma (~> 8.0) + redis sinatra (~> 4.1) CHECKSUMS @@ -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 diff --git a/frameworks/sinatra/app.rb b/frameworks/sinatra/app.rb index a8344679e..25569e66a 100644 --- a/frameworks/sinatra/app.rb +++ b/frameworks/sinatra/app.rb @@ -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' @@ -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) @@ -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 diff --git a/frameworks/sinatra/meta.json b/frameworks/sinatra/meta.json index e947080f6..4cb1a97b2 100644 --- a/frameworks/sinatra/meta.json +++ b/frameworks/sinatra/meta.json @@ -17,6 +17,7 @@ "api-4", "api-16", "async-db", + "crud", "static" ], "maintainers": ["p8"] From e086318f39add2be6cb6c4fa430535c207fdd584 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 3 Jun 2026 20:53:06 +0000 Subject: [PATCH 2/2] Benchmark results: sinatra crud --- site/data/crud-4096.json | 20 +++++++ site/static/logs/crud/4096/sinatra.log | 78 ++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 site/static/logs/crud/4096/sinatra.log diff --git a/site/data/crud-4096.json b/site/data/crud-4096.json index e6c88f2ce..350e32c7c 100644 --- a/site/data/crud-4096.json +++ b/site/data/crud-4096.json @@ -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", diff --git a/site/static/logs/crud/4096/sinatra.log b/site/static/logs/crud/4096/sinatra.log new file mode 100644 index 000000000..7595253af --- /dev/null +++ b/site/static/logs/crud/4096/sinatra.log @@ -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