Skip to content

Commit 5eb97c4

Browse files
authored
[ruby] Add rack-app (#10257)
rack-app is a minimalist web framework that focuses on simplicity and maintainability. The framework is meant to be used by seasoned web developers.
1 parent eeda59f commit 5eb97c4

File tree

11 files changed

+370
-0
lines changed

11 files changed

+370
-0
lines changed

frameworks/Ruby/rack-app/Gemfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
source 'https://rubygems.org'
4+
5+
gem 'rack-app'
6+
gem 'rack-app-front_end'
7+
gem 'iodine', '~> 0.7', platforms: %i[ruby windows]
8+
gem 'irb' # for Ruby 3.5
9+
gem 'logger' # for Ruby 3.5
10+
gem 'json', '~> 2.10'
11+
gem 'pg', '~> 1.5'
12+
gem 'sequel', '~> 5.0'
13+
gem 'sequel_pg', '~> 1.6', require: false
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
concurrent-ruby (1.3.5)
5+
date (3.5.0)
6+
erb (5.1.3)
7+
io-console (0.8.1)
8+
irb (1.15.3)
9+
pp (>= 0.6.0)
10+
rdoc (>= 4.0.0)
11+
reline (>= 0.4.2)
12+
json (2.15.2)
13+
logger (1.7.0)
14+
nio4r (2.7.5)
15+
pp (0.6.3)
16+
prettyprint
17+
prettyprint (0.2.0)
18+
psych (5.2.6)
19+
date
20+
stringio
21+
puma (7.1.0)
22+
nio4r (~> 2.0)
23+
rack (3.2.4)
24+
rack-app (11.0.2)
25+
rack (>= 3.0.0)
26+
rackup
27+
rackup (2.2.1)
28+
rack (>= 3)
29+
rdoc (6.15.1)
30+
erb
31+
psych (>= 4.0.0)
32+
tsort
33+
reline (0.6.2)
34+
io-console (~> 0.5)
35+
stringio (3.1.7)
36+
tsort (0.2.0)
37+
38+
PLATFORMS
39+
arm64-darwin-24
40+
ruby
41+
42+
DEPENDENCIES
43+
concurrent-ruby
44+
irb
45+
json (~> 2.10)
46+
logger
47+
puma (~> 7.1)
48+
rack-app
49+
50+
BUNDLED WITH
51+
2.7.2

frameworks/Ruby/rack-app/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Rack-app Benchmarking Test
2+
3+
rack-app is a minimalist web framework that focuses on simplicity and
4+
maintainability. The framework is meant to be used by seasoned web developers.
5+
6+
https://github.com/rack-app/rack-app
7+
8+
### Test Type Implementation Source Code
9+
10+
* [JSON Serialization](app.rb): "/json"
11+
* [Single Database Query](app.rb): "/db"
12+
* [Multiple Database Queries](app.rb): "/db?queries={#}"
13+
* [Fortunes](app.rb): "/fortune"
14+
* [Plaintext](app.rb): "/plaintext"
15+
16+
## Important Libraries
17+
18+
The tests were run with:
19+
20+
* [Sequel](https://rubygems.org/gems/sequel)
21+
* [PG](https://rubygems.org/gems/pg)
22+
23+
## Test URLs
24+
25+
### JSON
26+
27+
http://localhost:8080/json
28+
29+
### PLAINTEXT
30+
31+
http://localhost:8080/plaintext
32+
33+
### DB
34+
35+
http://localhost:8080/db
36+
37+
### QUERY
38+
39+
http://localhost:8080/queries?queries=
40+
41+
### FORTUNES
42+
43+
http://localhost:8080/fortunes
44+

frameworks/Ruby/rack-app/app.rb

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# frozen_string_literal: true
2+
3+
require 'rack/app'
4+
require 'rack/app/front_end'
5+
require 'json'
6+
7+
class App < Rack::App
8+
MAX_PK = 10_000
9+
ID_RANGE = (1..10_000).freeze
10+
ALL_IDS = ID_RANGE.to_a
11+
QUERIES_MIN = 1
12+
QUERIES_MAX = 500
13+
JSON_TYPE = 'application/json'
14+
HTML_TYPE = 'text/html; charset=utf-8'
15+
PLAINTEXT_TYPE = 'text/plain'
16+
17+
apply_extensions :front_end
18+
19+
helpers do
20+
def fortunes
21+
fortunes = Fortune.all
22+
fortunes << Fortune.new(
23+
id: 0,
24+
message: "Additional fortune added at request time."
25+
)
26+
fortunes.sort_by!(&:message)
27+
end
28+
end
29+
30+
get '/json' do
31+
set_headers(JSON_TYPE)
32+
{ message: 'Hello, World!' }.to_json
33+
end
34+
35+
get '/db' do
36+
set_headers(JSON_TYPE)
37+
World.with_pk(rand1).values.to_json
38+
end
39+
40+
get '/queries' do
41+
set_headers(JSON_TYPE)
42+
ids = ALL_IDS.sample(bounded_queries)
43+
DB.synchronize do
44+
ids.map do |id|
45+
World.with_pk(id).values
46+
end
47+
end.to_json
48+
end
49+
50+
get '/fortunes' do
51+
set_headers(HTML_TYPE)
52+
render 'fortunes.html.erb'
53+
end
54+
55+
get '/plaintext' do
56+
set_headers(PLAINTEXT_TYPE)
57+
'Hello, World!'
58+
end
59+
60+
private
61+
62+
# Return a random number between 1 and MAX_PK
63+
def rand1
64+
rand(MAX_PK).succ
65+
end
66+
67+
def bounded_queries
68+
queries = params['queries'].to_i
69+
queries.clamp(QUERIES_MIN, QUERIES_MAX)
70+
end
71+
72+
def set_headers(content_type)
73+
response.headers[::Rack::CONTENT_TYPE] = content_type
74+
response.headers['Server'] = 'rack-app'
75+
end
76+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head><title>Fortunes</title></head>
4+
<body>
5+
<table>
6+
<tr><th>id</th><th>message</th></tr>
7+
<% fortunes.each do |record| %>
8+
<tr><td><%= record.id %></td><td><%= ERB::Escape.html_escape(record.message) %></td></tr>
9+
<% end %>
10+
</table>
11+
</body>
12+
</html>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"framework": "rack-app",
3+
"tests": [
4+
{
5+
"default": {
6+
"json_url": "/json",
7+
"plaintext_url": "/plaintext",
8+
"db_url": "/db",
9+
"query_url": "/queries?queries=",
10+
"fortune_url": "/fortunes",
11+
"port": 8080,
12+
"approach": "Realistic",
13+
"classification": "Micro",
14+
"orm": "Full",
15+
"database": "Postgres",
16+
"framework": "rack-app",
17+
"language": "Ruby",
18+
"platform": "Mri",
19+
"webserver": "Iodine",
20+
"os": "Linux",
21+
"database_os": "Linux",
22+
"display_name": "rack-app",
23+
"notes": ""
24+
}
25+
}
26+
]
27+
}

frameworks/Ruby/rack-app/boot.rb

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# frozen_string_literal: true
2+
require 'bundler/setup'
3+
require 'time'
4+
5+
MAX_PK = 10_000
6+
ID_RANGE = (1..MAX_PK).freeze
7+
ALL_IDS = ID_RANGE.to_a
8+
QUERIES_MIN = 1
9+
QUERIES_MAX = 500
10+
SEQUEL_NO_ASSOCIATIONS = true
11+
#SERVER_STRING = "Sinatra"
12+
13+
Bundler.require(:default) # Load core modules
14+
15+
def connect(dbtype)
16+
Bundler.require(dbtype) # Load database-specific modules
17+
18+
opts = {}
19+
20+
adapter = 'postgresql'
21+
22+
# Determine threading/thread pool size and timeout
23+
if defined?(Puma) && (threads = Puma.cli_config.options.fetch(:max_threads)) > 1
24+
opts[:max_connections] = threads
25+
opts[:pool_timeout] = 10
26+
else
27+
opts[:max_connections] = 512
28+
end
29+
30+
Sequel.connect \
31+
'%{adapter}://%{host}/%{database}?user=%{user}&password=%{password}' % {
32+
adapter: adapter,
33+
host: 'tfb-database',
34+
database: 'hello_world',
35+
user: 'benchmarkdbuser',
36+
password: 'benchmarkdbpass'
37+
}, opts
38+
end
39+
40+
DB = connect 'postgres'
41+
42+
# Define ORM models
43+
class World < Sequel::Model(:World)
44+
def_column_alias(:randomnumber, :randomNumber) if DB.database_type == :mysql
45+
46+
def self.batch_update(worlds)
47+
if DB.database_type == :mysql
48+
worlds.map(&:save_changes)
49+
else
50+
ids = []
51+
sql = String.new("UPDATE world SET randomnumber = CASE id ")
52+
worlds.each do |world|
53+
sql << "when #{world.id} then #{world.randomnumber} "
54+
ids << world.id
55+
end
56+
sql << "ELSE randomnumber END WHERE id IN ( #{ids.join(',')})"
57+
DB.run(sql)
58+
end
59+
end
60+
end
61+
62+
class Fortune < Sequel::Model(:Fortune)
63+
# Allow setting id to zero (0) per benchmark requirements
64+
unrestrict_primary_key
65+
end
66+
67+
[World, Fortune].each(&:freeze)
68+
DB.freeze

frameworks/Ruby/rack-app/config.ru

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# frozen_string_literal: true
2+
require_relative 'boot'
3+
require_relative 'app'
4+
5+
run App
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Instantiate about one process per X MiB of available memory, scaling up to as
5+
# close to MAX_THREADS as possible while observing an upper bound based on the
6+
# number of virtual/logical CPUs. If there are fewer processes than
7+
# MAX_THREADS, add threads per process to reach MAX_THREADS.
8+
require 'etc'
9+
10+
KB_PER_WORKER = 64 * 1_024 # average of peak PSS of single-threaded processes (watch smem -k)
11+
MIN_WORKERS = 2
12+
MAX_WORKERS_PER_VCPU = 1.25 # virtual/logical
13+
MIN_THREADS_PER_WORKER = 1
14+
MAX_THREADS = Integer(ENV['MAX_CONCURRENCY'] || 256)
15+
16+
def meminfo(arg)
17+
File.open('/proc/meminfo') do |f|
18+
f.each_line do |line|
19+
key, value = line.split(/:\s+/)
20+
return value.split(/\s+/).first.to_i if key == arg
21+
end
22+
end
23+
24+
raise "Unable to find `#{arg}' in /proc/meminfo!"
25+
end
26+
27+
def auto_tune
28+
avail_mem = meminfo('MemAvailable') * 0.8 - MAX_THREADS * 1_024
29+
30+
workers = [
31+
[(1.0 * avail_mem / KB_PER_WORKER).floor, MIN_WORKERS].max,
32+
[(Etc.nprocessors * MAX_WORKERS_PER_VCPU).ceil, MIN_WORKERS].max
33+
].min
34+
35+
threads_per_worker = [
36+
workers < MAX_THREADS ? (1.0 * MAX_THREADS / workers).ceil : -Float::INFINITY,
37+
MIN_THREADS_PER_WORKER
38+
].max
39+
40+
[workers, threads_per_worker]
41+
end
42+
43+
p auto_tune if $PROGRAM_NAME == __FILE__
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
require_relative 'auto_tune'
2+
3+
# FWBM only... use the puma_auto_tune gem in production!
4+
_num_workers, num_threads = auto_tune
5+
6+
threads num_threads
7+
8+
before_fork do
9+
Sequel::DATABASES.each(&:disconnect)
10+
end

0 commit comments

Comments
 (0)