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
24 changes: 24 additions & 0 deletions frameworks/roda/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM ruby:4.0-slim

RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential libsqlite3-dev libjemalloc2 && \
rm -rf /var/lib/apt/lists/*

# Use Jemalloc
ENV LD_PRELOAD=libjemalloc.so.2

ENV RUBY_YJIT_ENABLE=1
ENV RUBY_MN_THREADS=1
ENV RACK_ENV=production
ENV WEB_CONCURRENCY=auto

WORKDIR /app

COPY Gemfile .
RUN bundle install --jobs=$(nproc)

COPY . .

EXPOSE 8080

CMD ["bundle", "exec", "puma", "-C", "puma.rb"]
7 changes: 7 additions & 0 deletions frameworks/roda/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
source 'https://rubygems.org'

gem 'roda', '~> 3.100'
gem 'puma', '~> 7.2'
gem 'sqlite3', '~> 2.9'
gem 'json'
gem 'concurrent-ruby'
119 changes: 119 additions & 0 deletions frameworks/roda/app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# frozen_string_literal: true

require 'bundler/setup'
Bundler.require(:default)

require 'zlib'

class App < Roda
# Load dataset
dataset_path = ENV.fetch('DATASET_PATH', '/data/dataset.json')
if File.exist?(dataset_path)
opts[:dataset_items] = JSON.parse(File.read(dataset_path))
end

# Large dataset for compression
large_path = '/data/dataset-large.json'
if File.exist?(large_path)
raw = JSON.parse(File.read(large_path))
items = raw.map do |d|
d.merge('total' => (d['price'] * d['quantity'] * 100).round / 100.0)
end
opts[:large_json_payload] = JSON.generate({ 'items' => items, 'count' => items.length })
end

# SQLite
opts[:db_available] = File.exist?('/data/benchmark.db')

DB_QUERY = 'SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT 50'

plugin :default_headers, 'Server' => 'roda'
plugin :halt
plugin :streaming

route do |r|
r.root { 'ok' }

r.is 'pipeline' do
response[RodaResponseHeaders::CONTENT_TYPE] = 'text/plain'
'ok'
end

r.is('baseline11') { handle_baseline11 }

r.is 'baseline2' do
total = 0
request.GET.each do |_k, v|
total += v.to_i
end
response[RodaResponseHeaders::CONTENT_TYPE] = 'text/plain'
total.to_s
end

r.is 'json' do
dataset = opts[:dataset_items]
r.halt 500, 'No dataset' unless dataset
items = dataset.map do |d|
d.merge('total' => (d['price'] * d['quantity'] * 100).round / 100.0)
end
response[RodaResponseHeaders::CONTENT_TYPE] = 'application/json'
JSON.generate({ 'items' => items, 'count' => items.length })
end

r.is 'compression' do
payload = opts[:large_json_payload]
r.halt 500, 'No dataset' unless payload
sio = StringIO.new
gz = Zlib::GzipWriter.new(sio, 1)
gz.write(payload)
gz.close
response[RodaResponseHeaders::CONTENT_TYPE] = 'application/json'
response[RodaResponseHeaders::CONTENT_ENCODING] = 'gzip'
sio.string
end

r.is 'db' do
unless opts[:db_available]
response[RodaResponseHeaders::CONTENT_TYPE] = 'application/json'
return '{"items":[],"count":0}'
end
min_val = (request.params['min'] || 10).to_i
max_val = (request.params['max'] || 50).to_i
db = get_db
rows = db.execute(DB_QUERY, [min_val, max_val])
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'] }
}
end
response[RodaResponseHeaders::CONTENT_TYPE] = 'application/json'
JSON.generate({ 'items' => items, 'count' => items.length })
end
end

def handle_baseline11
total = 0
request.GET.each do |_k, v|
total += v.to_i
end
if request.post?
request.body.rewind
body_str = request.body.read.strip
total += body_str.to_i
end
response[RodaResponseHeaders::CONTENT_TYPE] = 'text/plain'
total.to_s
end

def get_db
Thread.current[:roda_db] ||= begin
db = SQLite3::Database.new('/data/benchmark.db', readonly: true)
db.execute('PRAGMA mmap_size=268435456')
db.results_as_hash = true
db
end
end
end
21 changes: 21 additions & 0 deletions frameworks/roda/config.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require_relative 'app'

# Rack middleware to handle unknown HTTP methods before Puma/Sinatra
class MethodGuard
KNOWN = %w[GET POST PUT DELETE PATCH HEAD OPTIONS TRACE CONNECT].freeze

def initialize(app)
@app = app
end

def call(env)
if KNOWN.include?(env['REQUEST_METHOD'])
@app.call(env)
else
[405, { 'content-type' => 'text/plain', 'server' => 'roda' }, ['Method Not Allowed']]
end
end
end

use MethodGuard
run App
18 changes: 18 additions & 0 deletions frameworks/roda/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"display_name": "Roda",
"language": "Ruby",
"type": "framework",
"engine": "puma",
"description": "Roda routing tree web toolkit",
"repo": "https://github.com/jeremyevans/roda",
"enabled": true,
"tests": [
"baseline",
"pipelined",
"noisy",
"limited-conn",
"json",
"compression",
"mixed"
]
}
12 changes: 12 additions & 0 deletions frameworks/roda/puma.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
threads 4, 4

bind 'tcp://0.0.0.0:8080'

# Allow all HTTP methods so unknown ones reach Rack middleware (returned as 405)
supported_http_methods :any

preload_app!

before_fork do
# Close any inherited DB connections
end