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
2 changes: 2 additions & 0 deletions lessons/220/rails-app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@

# Ignore master key for decrypting credentials and more.
/config/master.key

/.idea
2 changes: 1 addition & 1 deletion lessons/220/rails-app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@ ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start the server by default, this can be overwritten at runtime
EXPOSE 8080
CMD ["./bin/rails", "server"]
CMD ["./bin/rails", "server", "-b", "0.0.0.0", "-p", "8080"]
47 changes: 6 additions & 41 deletions lessons/220/rails-app/Gemfile
Original file line number Diff line number Diff line change
@@ -1,47 +1,12 @@
source "https://rubygems.org"

gem 'pg'
gem "falcon"
gem "oj"
gem "pg"
gem "prometheus-client"
gem "rage-rb"

gem 'prometheus-client'

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.2.1", ">= 7.2.1.2"
# Use sqlite3 as the database for Active Record
gem "sqlite3", ">= 1.4"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
# gem "jbuilder"
# Use Redis adapter to run Action Cable in production
# gem "redis", ">= 4.0.1"

# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]
gem "rails"

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin Ajax possible
# gem "rack-cors"

group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"

# Static analysis for security vulnerabilities [https://brakemanscanner.org/]
gem "brakeman", require: false

# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
gem "rubocop-rails-omakase", require: false
end


This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
class ApplicationController < ActionController::API
class ApplicationController < ActionController::Metal
end
54 changes: 16 additions & 38 deletions lessons/220/rails-app/app/controllers/devices_controller.rb
Original file line number Diff line number Diff line change
@@ -1,56 +1,34 @@
class DevicesController < ApplicationController
before_action :set_device, only: %i[ show update destroy ]
# frozen_string_literal: true

class DevicesController < ApplicationController
# GET /devices
def index
devices = [
Device.new(id: 1, uuid: "9add349c-c35c-4d32-ab0f-53da1ba40a2a", mac: "5F-33-CC-1F-43-82", firmware: "2.1.6", created_at: "2024-5-28T15:21:51.137Z", updated_at: "2024-5-28T15:21:51.137Z"),
Device.new(id: 2, uuid: "d2293412-36eb-46e7-9231-af7e9249fffe", mac: "E7-34-96-33-0C-4C", firmware: "1.0.3", created_at: "2024-01-28T15:20:51.137Z", updated_at: "2024-01-28T15:20:51.137Z"),
Device.new(id: 3, uuid: "eee58ca8-ca51-47a5-ab48-163fd0e44b77", mac: "68-93-9B-B5-33-B9", firmware: "4.3.1", created_at: "2024-8-28T15:18:21.137Z", updated_at: "2024-8-28T15:18:21.137Z")
Device.new(id: 1, uuid: "9add349c-c35c-4d32-ab0f-53da1ba40a2a", mac: "5F-33-CC-1F-43-82", firmware: "2.1.6", created_at: "2024-5-28T15:21:51.137Z", updated_at: "2024-5-28T15:21:51.137Z").attributes,
Device.new(id: 2, uuid: "d2293412-36eb-46e7-9231-af7e9249fffe", mac: "E7-34-96-33-0C-4C", firmware: "1.0.3", created_at: "2024-01-28T15:20:51.137Z", updated_at: "2024-01-28T15:20:51.137Z").attributes,
Device.new(id: 3, uuid: "eee58ca8-ca51-47a5-ab48-163fd0e44b77", mac: "68-93-9B-B5-33-B9", firmware: "4.3.1", created_at: "2024-8-28T15:18:21.137Z", updated_at: "2024-8-28T15:18:21.137Z").attributes
Comment on lines +7 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antonputra @simpl1g what is the practical meaning of this benchmark?
for example, in node.js just creates hashes, we can do the same in ruby, and we don't need ActiveRecord objects, hash is much lighter and faster

devices = [
  { id: 1, uuid: "9add349c-c35c-4d32-ab0f-53da1ba40a2a", mac: "5F-33-CC-1F-43-82", firmware: "2.1.6", created_at: "2024-5-28T15:21:51.137Z", updated_at: "2024-5-28T15:21:51.137Z"}, 
  { id: 2, uuid: "d2293412-36eb-46e7-9231-af7e9249fffe", mac: "E7-34-96-33-0C-4C", firmware: "1.0.3", created_at: "2024-01-28T15:20:51.137Z", updated_at: "2024-01-28T15:20:51.137Z" },
  { id: 3, uuid: "eee58ca8-ca51-47a5-ab48-163fd0e44b77", mac: "68-93-9B-B5-33-B9", firmware: "4.3.1", created_at: "2024-8-28T15:18:21.137Z", updated_at: "2024-8-28T15:18:21.137Z" },
]

I think it's more correct to load records from postgresql here, it is close to real use

build_response(Device.find(1,2,3), 200)

@simpl1g simpl1g Nov 3, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it is just CPU bound benchmark, to test how well it handles serialisation. 2nd test is for IO.
But I agree that comparing objects with plain hash is not ideal in this case, that's why I added pure ruby example

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ermolaev well i'm going to test Rails with Django, and pure Ruby implementation from this PR with Node.js - #330

]

render json: devices
end

# GET /devices/1
def show
render json: @device
build_response(devices, 200)
end

# POST /devices
def create
@device = Device.new(device_params)
@device.uuid = SecureRandom.uuid

if @device.save
render json: @device, status: :created, location: @device
else
render json: @device.errors, status: :unprocessable_entity
end
end
device = Device.new(params.slice(:mac, :firmware))
device.uuid = SecureRandom.uuid

# PATCH/PUT /devices/1
def update
if @device.update(device_params)
render json: @device
if device.save

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when rails saving, query wrapped in a transaction, so here we have 3 queries to database

  TRANSACTION (1.2ms)  BEGIN
  Device Create (1.7ms)  INSERT INTO "devices" ("uuid", "mac", "firmware", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["uuid", nil], ["mac", "dfdf"], ["firmware", nil], ["created_at", "2024-11-06 00:53:09.435080"], ["updated_at", "2024-11-06 00:53:09.435080"]]
  TRANSACTION (0.6ms)  COMMIT

this code is sensitive latency to database

I do not know how other frameworks (django,laravel) works, but they can only insertion (1 query)

we can write device.send :create_or_update for skip transaction
it improves performance Requests/sec: 6866.0961 -> 9428.1055 on my mac m1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@simpl1g @antonputra any ideas about this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ermolaev yeah, it makes sense to disable transactions if Django also is not using them. It is better to check Django before I guess. But you can at least create PR

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep it in place for now.

build_response(device, 201)
else
render json: @device.errors, status: :unprocessable_entity
build_response(device.errors, 422)
end
end

# DELETE /devices/1
def destroy
@device.destroy!
end

private
# Use callbacks to share common setup or constraints between actions.
def set_device
@device = Device.find(params[:id])
end

# Only allow a list of trusted parameters through.
def device_params
params.require(:device).permit(:uuid, :mac, :firmware)
end
def build_response(body, status_code)
self.content_type = "application/json"
self.status = status_code
self.response_body = body.to_json
end
end
7 changes: 0 additions & 7 deletions lessons/220/rails-app/app/jobs/application_job.rb

This file was deleted.

4 changes: 0 additions & 4 deletions lessons/220/rails-app/app/mailers/application_mailer.rb

This file was deleted.

7 changes: 7 additions & 0 deletions lessons/220/rails-app/app/models/device.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
class Device < ApplicationRecord
def save
result = nil

PrometheusClient.db_histogram.observe(Benchmark.realtime { result = super })

result
end
end

This file was deleted.

13 changes: 0 additions & 13 deletions lessons/220/rails-app/app/views/layouts/mailer.html.erb

This file was deleted.

1 change: 0 additions & 1 deletion lessons/220/rails-app/app/views/layouts/mailer.text.erb

This file was deleted.

27 changes: 27 additions & 0 deletions lessons/220/rails-app/bin/iodine
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

#
# This file was generated by Bundler.
#
# The application 'iodine' is installed as part of a gem, and
# this file is here to facilitate running it.
#

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)

bundle_binstub = File.expand_path("bundle", __dir__)

if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end

require "rubygems"
require "bundler/setup"

load Gem.bin_path("rage-iodine", "iodine")
5 changes: 0 additions & 5 deletions lessons/220/rails-app/config.ru
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
# This file is used by Rack-based servers to start the application.
require 'rack'
require 'prometheus/middleware/collector'
require 'prometheus/middleware/exporter'

use Rack::Deflater
use Prometheus::Middleware::Collector
use Prometheus::Middleware::Exporter

run ->(_) { [200, {'content-type' => 'text/html'}, ['OK']] }

require_relative "config/environment"

run Rails.application
Expand Down
31 changes: 30 additions & 1 deletion lessons/220/rails-app/config/application.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require_relative "boot"

require "rails/all"
require "active_record/railtie"
require "action_controller/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Expand Down Expand Up @@ -28,5 +29,33 @@ class Application < Rails::Application
# Middleware like session, flash, cookies can be added back manually.
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
config.secret_key_base = 'secret'

config.middleware.delete Rack::Sendfile
config.middleware.delete ActionDispatch::Static
config.middleware.delete ActionDispatch::Executor
config.middleware.delete Rack::Runtime
config.middleware.delete ActionDispatch::RequestId
config.middleware.delete ActionDispatch::RemoteIp
config.middleware.delete Rails::Rack::Logger
config.middleware.delete ActionDispatch::ShowExceptions
config.middleware.delete ActionDispatch::DebugExceptions
config.middleware.delete ActionDispatch::Callbacks
config.middleware.delete Rack::Head
config.middleware.delete Rack::ConditionalGet
config.middleware.delete Rack::ETag

require 'rage/fiber'
require 'rage/fiber_scheduler'
require 'rage/middleware/fiber_wrapper'
config.middleware.use Rage::FiberWrapper

Oj.default_options = { mode: :compat }
Oj.optimize_rails

Iodine.workers = ENV.fetch('WORKERS_NUM', 2).to_i
Iodine.threads = 1

Iodine.on_state(:after_fork) { RubyVM::YJIT.enable }
end
end
10 changes: 0 additions & 10 deletions lessons/220/rails-app/config/cable.yml

This file was deleted.

1 change: 0 additions & 1 deletion lessons/220/rails-app/config/credentials.yml.enc

This file was deleted.

10 changes: 5 additions & 5 deletions lessons/220/rails-app/config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@
end

# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
# config.active_storage.service = :local

# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
# config.action_mailer.raise_delivery_errors = false

# Disable caching for Action Mailer templates even if Action Controller
# caching is enabled.
config.action_mailer.perform_caching = false
# config.action_mailer.perform_caching = false

config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
# config.action_mailer.default_url_options = { host: "localhost", port: 3000 }

# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
Expand All @@ -56,7 +56,7 @@
config.active_record.verbose_query_logs = true

# Highlight code that enqueued background job in logs.
config.active_job.verbose_enqueue_logs = true
# config.active_job.verbose_enqueue_logs = true

# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
Expand Down
Loading