From 78ec0c4a9e3316a35d7a748356cbb83cd678fb40 Mon Sep 17 00:00:00 2001 From: Petrik Date: Tue, 2 Jun 2026 09:50:47 +0200 Subject: [PATCH 1/2] [rails] Add crud --- frameworks/rails/Gemfile | 1 + frameworks/rails/Gemfile.lock | 7 ++ .../app/controllers/application_controller.rb | 45 +++++++ .../app/controllers/benchmark_controller.rb | 18 +-- .../rails/app/controllers/items_controller.rb | 110 ++++++++++++++++++ frameworks/rails/config/routes.rb | 4 + frameworks/rails/meta.json | 1 + 7 files changed, 169 insertions(+), 17 deletions(-) create mode 100644 frameworks/rails/app/controllers/application_controller.rb create mode 100644 frameworks/rails/app/controllers/items_controller.rb diff --git a/frameworks/rails/Gemfile b/frameworks/rails/Gemfile index 28f8346dc..a7cabee55 100644 --- a/frameworks/rails/Gemfile +++ b/frameworks/rails/Gemfile @@ -3,5 +3,6 @@ source 'https://rubygems.org' gem 'rails', '~> 8.0' gem 'puma', '~> 8.0' gem 'pg', '~> 1.5' +gem 'redis' gem 'bootsnap', require: false gem 'connection_pool' diff --git a/frameworks/rails/Gemfile.lock b/frameworks/rails/Gemfile.lock index 71a85dc2c..1d4ff6db5 100644 --- a/frameworks/rails/Gemfile.lock +++ b/frameworks/rails/Gemfile.lock @@ -183,6 +183,10 @@ GEM erb psych (>= 4.0.0) tsort + redis (5.4.1) + redis-client (>= 0.22.0) + redis-client (0.29.0) + connection_pool reline (0.6.3) io-console (~> 0.5) securerandom (0.4.1) @@ -210,6 +214,7 @@ DEPENDENCIES pg (~> 1.5) puma (~> 8.0) rails (~> 8.0) + redis CHECKSUMS action_text-trix (2.1.18) sha256=3fdb83f8bff4145d098be283cdd47ac41caf5110bfa6df4695ed7127d7fb3642 @@ -272,6 +277,8 @@ CHECKSUMS railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22 rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192 + redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae + redis-client (0.29.0) sha256=0c65bf1f8f6dca22063ddb085c0bb2054feef6f03a84869f4161b18a9a15bea3 reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 diff --git a/frameworks/rails/app/controllers/application_controller.rb b/frameworks/rails/app/controllers/application_controller.rb new file mode 100644 index 000000000..05401f9f3 --- /dev/null +++ b/frameworks/rails/app/controllers/application_controller.rb @@ -0,0 +1,45 @@ +class ApplicationController < ActionController::API + + 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" + 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 + + private + + def self.get_async_db + @async_db ||= begin + return unless ENV['DATABASE_URL'] + ConnectionPool.new(size: pool_size, timeout: 5) do + db = PG.connect(ENV['DATABASE_URL']) + 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('RAILS_MAX_THREADS', 4).to_i + end +end diff --git a/frameworks/rails/app/controllers/benchmark_controller.rb b/frameworks/rails/app/controllers/benchmark_controller.rb index 633e71580..a0c539ae1 100644 --- a/frameworks/rails/app/controllers/benchmark_controller.rb +++ b/frameworks/rails/app/controllers/benchmark_controller.rb @@ -1,7 +1,7 @@ require 'zlib' require 'pg' -class BenchmarkController < ActionController::API +class BenchmarkController < ApplicationController mattr_accessor :dataset DATA_DIR = ENV.fetch('DATA_DIR', '/data') @@ -14,8 +14,6 @@ class BenchmarkController < ActionController::API FileUtils.cp_r(File.join(DATA_DIR, 'static'), Rails.root.join('public', 'static')) - 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 - def baseline11 total = params[:a].to_i + params[:b].to_i if request.post? @@ -77,18 +75,4 @@ def upload def not_found head 404 end - - private - - def self.get_async_db - @async_db ||= begin - return unless ENV['DATABASE_URL'].present? - max_connections = ENV.fetch('RAILS_MAX_THREADS', 4).to_i - ConnectionPool.new(size: max_connections, timeout: 5) do - db = PG.connect(ENV['DATABASE_URL']) - db.prepare('select', PG_QUERY) - db - end - end - end end diff --git a/frameworks/rails/app/controllers/items_controller.rb b/frameworks/rails/app/controllers/items_controller.rb new file mode 100644 index 000000000..a5a78128a --- /dev/null +++ b/frameworks/rails/app/controllers/items_controller.rb @@ -0,0 +1,110 @@ +class ItemsController < ApplicationController + def index + category = params[:category] || 'electronics' + page = (params[:page] || 1).to_i + limit = (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: { items: items, total: items.length, page: page, limit: limit } + end + + def show + id = params[:id] + json = self.class.redis&.with do |connection| + connection.get(id.to_s) + end + if json + headers['x-cache'] = 'HIT' + return render json: json + else + headers['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: item + else + head 404 + end + end + + def create + 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 + } + + render json: item, status: 201 + end + + def update + 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: item + end + + private + + 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 +end diff --git a/frameworks/rails/config/routes.rb b/frameworks/rails/config/routes.rb index 9b5199ed3..53f2879dd 100644 --- a/frameworks/rails/config/routes.rb +++ b/frameworks/rails/config/routes.rb @@ -10,6 +10,10 @@ get '/json/:count', to: 'benchmark#json_endpoint' get '/async-db', to: 'benchmark#async_db' post '/upload', to: 'benchmark#upload' + get '/crud/items', to: 'items#index' + get '/crud/items/:id', to: "items#show" + post '/crud/items', to: 'items#create' + put '/crud/items/:id', to: 'items#update' # Catch-all for unknown paths → 404 match '*path', to: 'benchmark#not_found', via: :all diff --git a/frameworks/rails/meta.json b/frameworks/rails/meta.json index 6848a72a7..4ecb5e392 100644 --- a/frameworks/rails/meta.json +++ b/frameworks/rails/meta.json @@ -17,6 +17,7 @@ "api-4", "api-16", "async-db", + "crud", "static" ], "maintainers": ["p8"] From 685c28aab79780bfeb757ce5de0cc367ebd1eaab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Jun 2026 19:03:02 +0000 Subject: [PATCH 2/2] Benchmark results: rails crud --- site/data/crud-4096.json | 20 +++++++ site/static/logs/crud/4096/rails.log | 81 ++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 site/static/logs/crud/4096/rails.log diff --git a/site/data/crud-4096.json b/site/data/crud-4096.json index f206aa91c..47b4993c5 100644 --- a/site/data/crud-4096.json +++ b/site/data/crud-4096.json @@ -137,6 +137,26 @@ "status_4xx": 0, "status_5xx": 0 }, + { + "framework": "rails", + "language": "Ruby", + "rps": 61399, + "avg_latency": "64.18ms", + "p99_latency": "90.80ms", + "cpu": "4424.0%", + "memory": "4.3GiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "27.38MB/s", + "input_bw": "5.27MB/s", + "reconnects": 3877, + "status_2xx": 920997, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, { "framework": "ring-http-exchange", "language": "Clojure", diff --git a/site/static/logs/crud/4096/rails.log b/site/static/logs/crud/4096/rails.log new file mode 100644 index 000000000..a43557eaf --- /dev/null +++ b/site/static/logs/crud/4096/rails.log @@ -0,0 +1,81 @@ +=> Booting Puma +=> Rails 8.1.3 application starting in production +=> Run `bin/rails server --help` for more startup options +[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.15s, phase: 0 +[1] - Worker 1 (PID: 13) booted in 0.15s, phase: 0 +[1] - Worker 2 (PID: 17) booted in 0.15s, phase: 0 +[1] - Worker 3 (PID: 21) booted in 0.15s, phase: 0 +[1] - Worker 4 (PID: 25) booted in 0.14s, phase: 0 +[1] - Worker 5 (PID: 30) booted in 0.14s, phase: 0 +[1] - Worker 6 (PID: 35) booted in 0.14s, phase: 0 +[1] - Worker 7 (PID: 40) booted in 0.14s, phase: 0 +[1] - Worker 8 (PID: 45) booted in 0.13s, phase: 0 +[1] - Worker 9 (PID: 50) booted in 0.13s, phase: 0 +[1] - Worker 10 (PID: 57) booted in 0.13s, phase: 0 +[1] - Worker 11 (PID: 61) booted in 0.13s, phase: 0 +[1] - Worker 12 (PID: 68) booted in 0.13s, phase: 0 +[1] - Worker 13 (PID: 75) booted in 0.12s, phase: 0 +[1] - Worker 14 (PID: 82) booted in 0.12s, phase: 0 +[1] - Worker 15 (PID: 88) booted in 0.12s, phase: 0 +[1] - Worker 16 (PID: 94) booted in 0.12s, phase: 0 +[1] - Worker 17 (PID: 99) booted in 0.12s, phase: 0 +[1] - Worker 18 (PID: 105) booted in 0.11s, phase: 0 +[1] - Worker 19 (PID: 110) booted in 0.11s, phase: 0 +[1] - Worker 20 (PID: 116) booted in 0.11s, phase: 0 +[1] - Worker 21 (PID: 122) booted in 0.11s, phase: 0 +[1] - Worker 22 (PID: 127) booted in 0.11s, phase: 0 +[1] - Worker 23 (PID: 133) booted in 0.11s, phase: 0 +[1] - Worker 24 (PID: 138) booted in 0.1s, phase: 0 +[1] - Worker 25 (PID: 146) booted in 0.1s, phase: 0 +[1] - Worker 26 (PID: 152) booted in 0.1s, phase: 0 +[1] - Worker 27 (PID: 158) booted in 0.1s, phase: 0 +[1] - Worker 28 (PID: 164) booted in 0.1s, phase: 0 +[1] - Worker 29 (PID: 171) booted in 0.09s, phase: 0 +[1] - Worker 30 (PID: 180) booted in 0.09s, phase: 0 +[1] - Worker 31 (PID: 185) booted in 0.09s, phase: 0 +[1] - Worker 32 (PID: 190) booted in 0.08s, phase: 0 +[1] - Worker 33 (PID: 195) booted in 0.08s, phase: 0 +[1] - Worker 34 (PID: 202) booted in 0.08s, phase: 0 +[1] - Worker 35 (PID: 206) booted in 0.08s, phase: 0 +[1] - Worker 36 (PID: 212) booted in 0.08s, phase: 0 +[1] - Worker 37 (PID: 217) booted in 0.08s, phase: 0 +[1] - Worker 38 (PID: 222) booted in 0.07s, phase: 0 +[1] - Worker 39 (PID: 228) booted in 0.07s, phase: 0 +[1] - Worker 40 (PID: 233) booted in 0.07s, phase: 0 +[1] - Worker 41 (PID: 240) booted in 0.07s, phase: 0 +[1] - Worker 42 (PID: 245) booted in 0.06s, phase: 0 +[1] - Worker 43 (PID: 251) booted in 0.06s, phase: 0 +[1] - Worker 44 (PID: 257) booted in 0.06s, phase: 0 +[1] - Worker 45 (PID: 263) booted in 0.06s, phase: 0 +[1] - Worker 46 (PID: 269) booted in 0.06s, phase: 0 +[1] - Worker 47 (PID: 275) booted in 0.05s, phase: 0 +[1] - Worker 48 (PID: 281) booted in 0.05s, phase: 0 +[1] - Worker 49 (PID: 287) booted in 0.05s, phase: 0 +[1] - Worker 50 (PID: 293) booted in 0.05s, phase: 0 +[1] - Worker 51 (PID: 299) booted in 0.05s, phase: 0 +[1] - Worker 52 (PID: 306) booted in 0.04s, phase: 0 +[1] - Worker 53 (PID: 311) booted in 0.04s, phase: 0 +[1] - Worker 54 (PID: 318) booted in 0.04s, phase: 0 +[1] - Worker 55 (PID: 323) booted in 0.04s, phase: 0 +[1] - Worker 56 (PID: 330) booted in 0.04s, phase: 0 +[1] - Worker 57 (PID: 335) booted in 0.03s, phase: 0 +[1] - Worker 58 (PID: 342) booted in 0.03s, phase: 0 +[1] - Worker 59 (PID: 347) booted in 0.03s, phase: 0 +[1] - Worker 60 (PID: 353) booted in 0.03s, phase: 0 +[1] - Worker 61 (PID: 360) booted in 0.03s, phase: 0