Browse Source

[ruby/padrino] Upgrade to Ruby 3.4 (#9600)

Also use ActiveRecord instead of DataMapper, as that is no longer
maintained.

+-------------------+-----+-----+-----+------+-------+---------+--------------+
|        branch_name| json|   db|query|update|fortune|plaintext|weighted_score|
+-------------------+-----+-----+-----+------+-------+---------+--------------+
|             master|16348| 4309| 6315|  3785|   4251|    12539|           439|
|padrino/ruby-3.4-ar|35491|12931|12760|  7322|  10133|    17696|           880|
+-------------------+-----+-----+-----+------+-------+---------+--------------+
Petrik de Heus 5 months ago
parent
commit
37ec654ff7

+ 14 - 8
frameworks/Ruby/padrino/Gemfile

@@ -1,11 +1,17 @@
 source 'https://rubygems.org'
 source 'https://rubygems.org'
 
 
-gem 'mysql2', '~> 0.4'
-gem "unicorn", '~> 6.1'
-gem 'puma', '~> 6.4'
-gem 'json', '~> 2.0'
+gem 'mysql2', '> 0.5'
+gem 'json'
+gem 'activerecord', '>= 7.1', :require => 'active_record'
+
 gem 'slim', '2.0.3'
 gem 'slim', '2.0.3'
-gem 'dm-mysql-adapter', '1.2.0'
-gem 'dm-core', '1.2.1'
-gem 'padrino', '0.15.3'
-gem 'rack', '~> 2.2'
+gem 'padrino', git: 'https://github.com/padrino/padrino-framework.git'
+gem 'rack'
+
+group :puma, optional: true do
+  gem 'puma', '~> 6.4', require: false
+end
+
+group :unicorn, optional: true do
+  gem 'unicorn', '~> 6.1', platforms: [:ruby, :mswin], require: false
+end

+ 14 - 9
frameworks/Ruby/padrino/app/app.rb

@@ -14,11 +14,16 @@ module HelloWorld
     #
     #
     # You can customize caching store engines:
     # You can customize caching store engines:
     #
     #
-    # set :cache, Padrino::Cache::Store::Memcache.new(::Memcached.new('127.0.0.1:11211', :exception_retry_limit => 1))
-    # set :cache, Padrino::Cache::Store::Memcache.new(::Dalli::Client.new('127.0.0.1:11211', :exception_retry_limit => 1))
-    # set :cache, Padrino::Cache::Store::Redis.new(::Redis.new(:host => '127.0.0.1', :port => 6379, :db => 0))
-    # set :cache, Padrino::Cache::Store::Memory.new(50)
-    # set :cache, Padrino::Cache::Store::File.new(Padrino.root('tmp', app_name.to_s, 'cache')) # default choice
+    # set :cache, Padrino::Cache.new(:LRUHash) # Keeps cached values in memory
+    # set :cache, Padrino::Cache.new(:Memcached) # Uses default server at localhost
+    # set :cache, Padrino::Cache.new(:Memcached, :server => '127.0.0.1:11211', :exception_retry_limit => 1)
+    # set :cache, Padrino::Cache.new(:Memcached, :backend => memcached_or_dalli_instance)
+    # set :cache, Padrino::Cache.new(:Redis) # Uses default server at localhost
+    # set :cache, Padrino::Cache.new(:Redis, :host => '127.0.0.1', :port => 6379, :db => 0)
+    # set :cache, Padrino::Cache.new(:Redis, :backend => redis_instance)
+    # set :cache, Padrino::Cache.new(:Mongo) # Uses default server at localhost
+    # set :cache, Padrino::Cache.new(:Mongo, :backend => mongo_client_instance)
+    # set :cache, Padrino::Cache.new(:File, :dir => Padrino.root('tmp', app_name.to_s, 'cache')) # default choice
     #
     #
 
 
     ##
     ##
@@ -32,8 +37,8 @@ module HelloWorld
     # set :reload, false            # Reload application files (default in development)
     # set :reload, false            # Reload application files (default in development)
     # set :default_builder, 'foo'   # Set a custom form builder (default 'StandardFormBuilder')
     # set :default_builder, 'foo'   # Set a custom form builder (default 'StandardFormBuilder')
     # set :locale_path, 'bar'       # Set path for I18n translations (default your_apps_root_path/locale)
     # set :locale_path, 'bar'       # Set path for I18n translations (default your_apps_root_path/locale)
-    # disable :sessions             # Disabled sessions by default (enable if needed)
-    # disable :flash                # Disables sinatra-flash (enabled by default if Sinatra::Flash is defined)
+    disable :sessions             # Disabled sessions by default (enable if needed)
+    disable :flash                # Disables sinatra-flash (enabled by default if Sinatra::Flash is defined)
     # layout  :my_layout            # Layout can be in views/layouts/foo.ext or views/foo.ext (default :application)
     # layout  :my_layout            # Layout can be in views/layouts/foo.ext or views/foo.ext (default :application)
     #
     #
 
 
@@ -53,8 +58,8 @@ module HelloWorld
     #     render 'errors/404'
     #     render 'errors/404'
     #   end
     #   end
     #
     #
-    #   error 505 do
-    #     render 'errors/505'
+    #   error 500 do
+    #     render 'errors/500'
     #   end
     #   end
     #
     #
   end
   end

+ 37 - 34
frameworks/Ruby/padrino/app/controllers.rb

@@ -1,62 +1,65 @@
-MAX_PK = 10_000
-QUERIES_MIN = 1
-QUERIES_MAX = 500
+QUERY_RANGE = (1..10_000).freeze
+ALL_IDS = QUERY_RANGE.to_a
 
 
 HelloWorld::App.controllers  do
 HelloWorld::App.controllers  do
+
+  after do
+    response['Server'] = 'padrino'
+  end
+
+  after do
+    response['Date'] = Time.now.httpdate
+  end if defined?(Puma)
+
   get '/json', :provides => [:json] do
   get '/json', :provides => [:json] do
-    response.headers['Server'] = 'padrino'
-    response.headers['Date'] = Time.now.httpdate
     {message: "Hello, World!"}.to_json
     {message: "Hello, World!"}.to_json
   end
   end
 
 
   get '/db', :provides => [:json] do
   get '/db', :provides => [:json] do
-    response.headers['Server'] = 'padrino'
-    response.headers['Date'] = Time.now.httpdate
-    id = Random.rand(MAX_PK) + 1
-    World.get(id).attributes.to_json
+    world = ActiveRecord::Base.with_connection do
+      World.find(rand1).attributes
+    end
+    world.to_json
   end
   end
 
 
   get '/queries', :provides => [:json] do
   get '/queries', :provides => [:json] do
-    response.headers['Server'] = 'padrino'
-    response.headers['Date'] = Time.now.httpdate
-    queries = params['queries'].to_i.clamp(QUERIES_MIN, QUERIES_MAX)
-
-    results = (1..queries).map do
-      World.get(Random.rand(MAX_PK) + 1).attributes
-    end.to_json
+    worlds = ActiveRecord::Base.with_connection do
+      ALL_IDS.sample(bounded_queries).map do |id|
+        World.find(id).attributes
+      end
+    end
+    worlds.to_json
   end
   end
 
 
   get '/fortunes' do
   get '/fortunes' do
-    response.headers['Server'] = 'padrino'
-    response.headers['Date'] = Time.now.httpdate
-    @fortunes = Fortune.all
-    @fortunes << Fortune.new(:id => 0, :message => "Additional fortune added at request time.")
-    @fortunes = @fortunes.sort_by { |x| x.message }
+    @fortunes = Fortune.all.to_a
+    @fortunes << Fortune.new(
+      id: 0,
+      message: "Additional fortune added at request time."
+    )
+    @fortunes = @fortunes.sort_by(&:message)
 
 
     render 'fortunes', layout: "layout"
     render 'fortunes', layout: "layout"
   end
   end
 
 
   get '/updates', :provides => [:json] do
   get '/updates', :provides => [:json] do
-    response.headers['Server'] = 'padrino'
-    response.headers['Date'] = Time.now.httpdate
-    queries = params['queries'].to_i.clamp(QUERIES_MIN, QUERIES_MAX)
-
-    worlds = (1..queries).map do
-      # get a random row from the database, which we know has 10000
-      # rows with ids 1 - 10000
-      world = World.get(Random.rand(MAX_PK) + 1)
-      world.update(randomNumber: Random.rand(MAX_PK) + 1)
-      world.attributes
+    worlds = []
+    ActiveRecord::Base.with_connection do
+      worlds = ALL_IDS.sample(bounded_queries).map do |id|
+        world = World.find(id)
+        new_value = rand1
+        new_value = rand1 while new_value == world.randomNumber
+        world.randomNumber = new_value
+        world
+      end
+      World.upsert_all(worlds)
     end
     end
 
 
     worlds.to_json
     worlds.to_json
   end
   end
 
 
   get '/plaintext' do
   get '/plaintext' do
-    response.headers['Server'] = 'padrino'
-    response.headers['Date'] = Time.now.httpdate
     content_type 'text/plain'
     content_type 'text/plain'
     "Hello, World!"
     "Hello, World!"
   end
   end
-
 end
 end

+ 12 - 3
frameworks/Ruby/padrino/app/helpers.rb

@@ -1,7 +1,16 @@
 # Helper methods defined here can be accessed in any controller or view in the application
 # Helper methods defined here can be accessed in any controller or view in the application
 
 
+MAX_PK = 10_000
+QUERIES_MIN = 1
+QUERIES_MAX = 500
+
 HelloWorld::App.helpers do
 HelloWorld::App.helpers do
-  # def simple_helper_method
-  #  ...
-  # end
+  def rand1
+    rand(MAX_PK) + 1
+  end
+
+  def bounded_queries
+    queries = params[:queries].to_i
+    queries.clamp(QUERIES_MIN, QUERIES_MAX)
+  end
 end
 end

+ 3 - 3
frameworks/Ruby/padrino/benchmark_config.json

@@ -25,10 +25,10 @@
     },
     },
     "unicorn": {
     "unicorn": {
       "json_url": "/json",
       "json_url": "/json",
-      "db_url": "/db", 
-      "query_url": "/queries?queries=", 
+      "db_url": "/db",
+      "query_url": "/queries?queries=",
       "fortune_url": "/fortunes",
       "fortune_url": "/fortunes",
-      "update_url": "/updates?queries=", 
+      "update_url": "/updates?queries=",
       "plaintext_url": "/plaintext",
       "plaintext_url": "/plaintext",
       "port": 8080,
       "port": 8080,
       "approach": "Realistic",
       "approach": "Realistic",

+ 2 - 2
frameworks/Ruby/padrino/config/apps.rb

@@ -28,8 +28,8 @@
 Padrino.configure_apps do
 Padrino.configure_apps do
   # enable :sessions
   # enable :sessions
   set :session_secret, 'b941cbcb2d647360c0d1fb3c54a7039ed4f71cc0b7785d2aac689cc37d7757b7'
   set :session_secret, 'b941cbcb2d647360c0d1fb3c54a7039ed4f71cc0b7785d2aac689cc37d7757b7'
-  set :protection, true
-  set :protect_from_csrf, true
+  set :protection, false
+  set :protect_from_csrf, false
 end
 end
 
 
 # Mounts the core application for this project
 # Mounts the core application for this project

+ 0 - 1
frameworks/Ruby/padrino/config/boot.rb

@@ -39,7 +39,6 @@ end
 # Add your after (RE)load hooks here
 # Add your after (RE)load hooks here
 #
 #
 Padrino.after_load do
 Padrino.after_load do
-  DataMapper.finalize
 end
 end
 
 
 Padrino.load!
 Padrino.load!

+ 30 - 15
frameworks/Ruby/padrino/config/database.rb

@@ -1,17 +1,32 @@
-##
-# A MySQL connection:
-# DataMapper.setup(:default, 'mysql://user:password@localhost/the_database_name')
-#
-# # A Postgres connection:
-# DataMapper.setup(:default, 'postgres://user:password@localhost/the_database_name')
-#
-# # A Sqlite3 connection
-# DataMapper.setup(:default, "sqlite3://" + Padrino.root('db', "development.db"))
-#
+Bundler.require('mysql')
+opts = {
+  adapter:  'mysql2',
+  username: 'benchmarkdbuser',
+  password: 'benchmarkdbpass',
+  host:     'tfb-database',
+  database: 'hello_world'
+}
 
 
-DataMapper.logger = logger
-DataMapper::Property::String.length(255)
-
-case Padrino.env
-  when :production  then DataMapper.setup(:default, "mysql://benchmarkdbuser:benchmarkdbpass@tfb-database/hello_world")
+# Determine threading/thread pool size and timeout
+if defined?(Puma) && (threads = Puma.cli_config.options.fetch(:max_threads)) > 1
+  opts[:pool] = (2 * Math.log(threads)).floor
+  opts[:checkout_timeout] = 10
+else
+  # TODO: ActiveRecord doesn't have a single-threaded mode?
+  opts[:pool] = 1
+  opts[:checkout_timeout] = 0
 end
 end
+
+
+# Setup our logger
+ActiveRecord::Base.logger = logger
+
+# Use ISO 8601 format for JSON serialized times and dates.
+ActiveSupport.use_standard_json_time_format = true
+
+# Don't escape HTML entities in JSON, leave that for the #json_escape helper
+# if you're including raw JSON in an HTML page.
+ActiveSupport.escape_html_entities_in_json = false
+
+# Now we can establish connection with our db.
+ActiveRecord::Base.establish_connection(opts)

+ 2 - 8
frameworks/Ruby/padrino/models/fortune.rb

@@ -1,9 +1,3 @@
-class Fortune
-  include DataMapper::Resource
-
-  storage_names[:default] = 'Fortune'
-
-  # property <name>, <type>
-  property :id, Serial
-  property :message, String
+class Fortune < ActiveRecord::Base
+  self.table_name = name
 end
 end

+ 13 - 6
frameworks/Ruby/padrino/models/world.rb

@@ -1,9 +1,16 @@
-class World
-  include DataMapper::Resource
+class World < ActiveRecord::Base
+  self.table_name = name
 
 
-  storage_names[:default] = 'World'
+  alias_attribute(:randomNumber, :randomnumber) \
+    if connection.adapter_name.downcase.start_with?('postgres')
 
 
-  # property <name>, <type>
-  property :id, Serial
-  property :randomNumber, Integer, field: 'randomNumber'
+  if connection.adapter_name.downcase.start_with?('mysql')
+    def self.upsert_all(attributes, on_duplicate: :update, update_only: nil, returning: nil, unique_by: nil, record_timestamps: nil)
+      # On MySQL Batch updates verification isn't supported yet by TechEmpower.
+      # https://github.com/TechEmpower/FrameworkBenchmarks/issues/5983
+      attributes.each do |attrs|
+        where(id: attrs[:id]).update_all(randomNumber: attrs[:randomNumber])
+      end
+    end
+  end
 end
 end

+ 4 - 1
frameworks/Ruby/padrino/padrino-unicorn.dockerfile

@@ -1,4 +1,4 @@
-FROM ruby:3.0
+FROM ruby:3.4
 
 
 WORKDIR /padrino
 WORKDIR /padrino
 COPY app app
 COPY app app
@@ -9,11 +9,14 @@ COPY config.ru config.ru
 COPY Gemfile Gemfile
 COPY Gemfile Gemfile
 COPY Rakefile Rakefile
 COPY Rakefile Rakefile
 
 
+RUN bundle config set with 'unicorn'
 RUN bundle install --jobs=4 --gemfile=/padrino/Gemfile
 RUN bundle install --jobs=4 --gemfile=/padrino/Gemfile
 
 
 RUN apt-get update -yqq && apt-get install -yqq nginx
 RUN apt-get update -yqq && apt-get install -yqq nginx
 
 
 EXPOSE 8080
 EXPOSE 8080
 
 
+ENV RUBY_YJIT_ENABLE=1
+
 CMD nginx -c /padrino/config/nginx.conf && \
 CMD nginx -c /padrino/config/nginx.conf && \
     bundle exec unicorn -E production -c config/unicorn.rb
     bundle exec unicorn -E production -c config/unicorn.rb

+ 5 - 2
frameworks/Ruby/padrino/padrino.dockerfile

@@ -1,4 +1,4 @@
-FROM ruby:3.0
+FROM ruby:3.4
 
 
 WORKDIR /padrino
 WORKDIR /padrino
 COPY app app
 COPY app app
@@ -9,8 +9,11 @@ COPY config.ru config.ru
 COPY Gemfile Gemfile
 COPY Gemfile Gemfile
 COPY Rakefile Rakefile
 COPY Rakefile Rakefile
 
 
+RUN bundle config set with 'puma'
 RUN bundle install --jobs=4 --gemfile=/padrino/Gemfile
 RUN bundle install --jobs=4 --gemfile=/padrino/Gemfile
 
 
 EXPOSE 8080
 EXPOSE 8080
 
 
-CMD ["bundle", "exec", "puma", "-C", "config/puma.rb", "-w", "8", "--preload"]
+ENV RUBY_YJIT_ENABLE=1
+
+CMD bundle exec puma -C config/puma.rb -w 8 --preload