Browse Source

[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.
Petrik de Heus 1 month ago
parent
commit
5eb97c441a

+ 13 - 0
frameworks/Ruby/rack-app/Gemfile

@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+source 'https://rubygems.org'
+
+gem 'rack-app'
+gem 'rack-app-front_end'
+gem 'iodine', '~> 0.7', platforms: %i[ruby windows]
+gem 'irb' # for Ruby 3.5
+gem 'logger' # for Ruby 3.5
+gem 'json', '~> 2.10'
+gem 'pg', '~> 1.5'
+gem 'sequel', '~> 5.0'
+gem 'sequel_pg', '~> 1.6', require: false

+ 51 - 0
frameworks/Ruby/rack-app/Gemfile.lock

@@ -0,0 +1,51 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    concurrent-ruby (1.3.5)
+    date (3.5.0)
+    erb (5.1.3)
+    io-console (0.8.1)
+    irb (1.15.3)
+      pp (>= 0.6.0)
+      rdoc (>= 4.0.0)
+      reline (>= 0.4.2)
+    json (2.15.2)
+    logger (1.7.0)
+    nio4r (2.7.5)
+    pp (0.6.3)
+      prettyprint
+    prettyprint (0.2.0)
+    psych (5.2.6)
+      date
+      stringio
+    puma (7.1.0)
+      nio4r (~> 2.0)
+    rack (3.2.4)
+    rack-app (11.0.2)
+      rack (>= 3.0.0)
+      rackup
+    rackup (2.2.1)
+      rack (>= 3)
+    rdoc (6.15.1)
+      erb
+      psych (>= 4.0.0)
+      tsort
+    reline (0.6.2)
+      io-console (~> 0.5)
+    stringio (3.1.7)
+    tsort (0.2.0)
+
+PLATFORMS
+  arm64-darwin-24
+  ruby
+
+DEPENDENCIES
+  concurrent-ruby
+  irb
+  json (~> 2.10)
+  logger
+  puma (~> 7.1)
+  rack-app
+
+BUNDLED WITH
+   2.7.2

+ 44 - 0
frameworks/Ruby/rack-app/README.md

@@ -0,0 +1,44 @@
+# Rack-app Benchmarking Test
+
+rack-app is a minimalist web framework that focuses on simplicity and
+maintainability. The framework is meant to be used by seasoned web developers.
+
+https://github.com/rack-app/rack-app
+
+### Test Type Implementation Source Code
+
+* [JSON Serialization](app.rb): "/json"
+* [Single Database Query](app.rb): "/db"
+* [Multiple Database Queries](app.rb): "/db?queries={#}"
+* [Fortunes](app.rb): "/fortune"
+* [Plaintext](app.rb): "/plaintext"
+
+## Important Libraries
+
+The tests were run with:
+
+* [Sequel](https://rubygems.org/gems/sequel)
+* [PG](https://rubygems.org/gems/pg)
+
+## Test URLs
+
+### JSON
+
+http://localhost:8080/json
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext
+
+### DB
+
+http://localhost:8080/db
+
+### QUERY
+
+http://localhost:8080/queries?queries=
+
+### FORTUNES
+
+http://localhost:8080/fortunes
+

+ 76 - 0
frameworks/Ruby/rack-app/app.rb

@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'rack/app'
+require 'rack/app/front_end'
+require 'json'
+
+class App < Rack::App
+  MAX_PK = 10_000
+  ID_RANGE = (1..10_000).freeze
+  ALL_IDS = ID_RANGE.to_a
+  QUERIES_MIN = 1
+  QUERIES_MAX = 500
+  JSON_TYPE = 'application/json'
+  HTML_TYPE = 'text/html; charset=utf-8'
+  PLAINTEXT_TYPE = 'text/plain'
+
+  apply_extensions :front_end
+
+  helpers do
+    def fortunes
+      fortunes = Fortune.all
+      fortunes << Fortune.new(
+        id: 0,
+        message: "Additional fortune added at request time."
+      )
+      fortunes.sort_by!(&:message)
+    end
+  end
+
+  get '/json' do
+    set_headers(JSON_TYPE)
+    { message: 'Hello, World!' }.to_json
+  end
+
+  get '/db' do
+    set_headers(JSON_TYPE)
+    World.with_pk(rand1).values.to_json
+  end
+
+  get '/queries' do
+    set_headers(JSON_TYPE)
+    ids = ALL_IDS.sample(bounded_queries)
+    DB.synchronize do
+      ids.map do |id|
+        World.with_pk(id).values
+      end
+    end.to_json
+  end
+
+  get '/fortunes' do
+    set_headers(HTML_TYPE)
+    render 'fortunes.html.erb'
+  end
+
+  get '/plaintext' do
+    set_headers(PLAINTEXT_TYPE)
+    'Hello, World!'
+  end
+
+  private
+
+  # Return a random number between 1 and MAX_PK
+  def rand1
+    rand(MAX_PK).succ
+  end
+
+  def bounded_queries
+    queries = params['queries'].to_i
+    queries.clamp(QUERIES_MIN, QUERIES_MAX)
+  end
+
+  def set_headers(content_type)
+    response.headers[::Rack::CONTENT_TYPE] = content_type
+    response.headers['Server'] = 'rack-app'
+  end
+end

+ 12 - 0
frameworks/Ruby/rack-app/app/fortunes.html.erb

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+  <head><title>Fortunes</title></head>
+  <body>
+    <table>
+    <tr><th>id</th><th>message</th></tr>
+    <% fortunes.each do |record| %>
+      <tr><td><%= record.id %></td><td><%= ERB::Escape.html_escape(record.message) %></td></tr>
+    <% end %>
+    </table>
+  </body>
+</html>

+ 27 - 0
frameworks/Ruby/rack-app/benchmark_config.json

@@ -0,0 +1,27 @@
+{
+  "framework": "rack-app",
+  "tests": [
+    {
+      "default": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "fortune_url": "/fortunes",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "orm": "Full",
+        "database": "Postgres",
+        "framework": "rack-app",
+        "language": "Ruby",
+        "platform": "Mri",
+        "webserver": "Iodine",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "rack-app",
+        "notes": ""
+      }
+    }
+  ]
+}

+ 68 - 0
frameworks/Ruby/rack-app/boot.rb

@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+require 'bundler/setup'
+require 'time'
+
+MAX_PK = 10_000
+ID_RANGE = (1..MAX_PK).freeze
+ALL_IDS = ID_RANGE.to_a
+QUERIES_MIN = 1
+QUERIES_MAX = 500
+SEQUEL_NO_ASSOCIATIONS = true
+#SERVER_STRING = "Sinatra"
+
+Bundler.require(:default) # Load core modules
+
+def connect(dbtype)
+  Bundler.require(dbtype) # Load database-specific modules
+
+  opts = {}
+
+  adapter = 'postgresql'
+
+  # Determine threading/thread pool size and timeout
+  if defined?(Puma) && (threads = Puma.cli_config.options.fetch(:max_threads)) > 1
+    opts[:max_connections] = threads
+    opts[:pool_timeout] = 10
+  else
+    opts[:max_connections] = 512
+  end
+
+  Sequel.connect \
+    '%{adapter}://%{host}/%{database}?user=%{user}&password=%{password}' % {
+      adapter: adapter,
+      host: 'tfb-database',
+      database: 'hello_world',
+      user: 'benchmarkdbuser',
+      password: 'benchmarkdbpass'
+    }, opts
+end
+
+DB = connect 'postgres'
+
+# Define ORM models
+class World < Sequel::Model(:World)
+  def_column_alias(:randomnumber, :randomNumber) if DB.database_type == :mysql
+
+  def self.batch_update(worlds)
+    if DB.database_type == :mysql
+      worlds.map(&:save_changes)
+    else
+      ids = []
+      sql = String.new("UPDATE world SET randomnumber = CASE id ")
+      worlds.each do |world|
+        sql << "when #{world.id} then #{world.randomnumber} "
+        ids << world.id
+      end
+      sql << "ELSE randomnumber END WHERE id IN ( #{ids.join(',')})"
+      DB.run(sql)
+    end
+  end
+end
+
+class Fortune < Sequel::Model(:Fortune)
+  # Allow setting id to zero (0) per benchmark requirements
+  unrestrict_primary_key
+end
+
+[World, Fortune].each(&:freeze)
+DB.freeze

+ 5 - 0
frameworks/Ruby/rack-app/config.ru

@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+require_relative 'boot'
+require_relative 'app'
+
+run App

+ 43 - 0
frameworks/Ruby/rack-app/config/auto_tune.rb

@@ -0,0 +1,43 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+# Instantiate about one process per X MiB of available memory, scaling up to as
+# close to MAX_THREADS as possible while observing an upper bound based on the
+# number of virtual/logical CPUs. If there are fewer processes than
+# MAX_THREADS, add threads per process to reach MAX_THREADS.
+require 'etc'
+
+KB_PER_WORKER = 64 * 1_024 # average of peak PSS of single-threaded processes (watch smem -k)
+MIN_WORKERS = 2
+MAX_WORKERS_PER_VCPU = 1.25 # virtual/logical
+MIN_THREADS_PER_WORKER = 1
+MAX_THREADS = Integer(ENV['MAX_CONCURRENCY'] || 256)
+
+def meminfo(arg)
+  File.open('/proc/meminfo') do |f|
+    f.each_line do |line|
+      key, value = line.split(/:\s+/)
+      return value.split(/\s+/).first.to_i if key == arg
+    end
+  end
+
+  raise "Unable to find `#{arg}' in /proc/meminfo!"
+end
+
+def auto_tune
+  avail_mem = meminfo('MemAvailable') * 0.8 - MAX_THREADS * 1_024
+
+  workers = [
+    [(1.0 * avail_mem / KB_PER_WORKER).floor, MIN_WORKERS].max,
+    [(Etc.nprocessors * MAX_WORKERS_PER_VCPU).ceil, MIN_WORKERS].max
+  ].min
+
+  threads_per_worker = [
+    workers < MAX_THREADS ? (1.0 * MAX_THREADS / workers).ceil : -Float::INFINITY,
+    MIN_THREADS_PER_WORKER
+  ].max
+
+  [workers, threads_per_worker]
+end
+
+p auto_tune if $PROGRAM_NAME == __FILE__

+ 10 - 0
frameworks/Ruby/rack-app/config/puma.rb

@@ -0,0 +1,10 @@
+require_relative 'auto_tune'
+
+# FWBM only... use the puma_auto_tune gem in production!
+_num_workers, num_threads = auto_tune
+
+threads num_threads
+
+before_fork do
+  Sequel::DATABASES.each(&:disconnect)
+end

+ 21 - 0
frameworks/Ruby/rack-app/rack-app.dockerfile

@@ -0,0 +1,21 @@
+FROM ruby:3.5-rc
+
+ENV RUBY_YJIT_ENABLE=1
+
+# Use Jemalloc
+RUN apt-get update && \
+    apt-get install -y --no-install-recommends libjemalloc2
+ENV LD_PRELOAD=libjemalloc.so.2
+
+WORKDIR /rack-app
+
+COPY Gemfile* ./
+
+ENV BUNDLE_FORCE_RUBY_PLATFORM=true
+RUN bundle install --jobs=8
+
+COPY . .
+
+EXPOSE 8080
+
+CMD bundle exec iodine -p 8080 -w $(ruby config/auto_tune.rb | grep -Eo '[0-9]+' | head -n 1)