Rails: Fixing the memory leak / performance issues in prepend_view_path
Recently we detected a memory leak in one of our applications. Hunting it down, we found that the memory leak was located in Rails' #prepend_view_path. It occurs when the instance method prepend_view_path is called in each request, which is a common thing in a multi-tenant application.
On top of leaking memory, it also causes a performance hit, since templates rendered using the prepended view path will not be cached and compiled anew on each request.
This is not a new memory leak. It was [first reported in in 2014](https://github.com/...
Maintaining custom application tasks in Rails
Here are some hints on best practices to maintain your tasks in larger projects.
Rake Tasks vs. Scripts
- The Rails default is using rake tasks for your application tasks. These live in
lib/tasks/*. - In case you want to avoid rake for your tasks and just use plain ruby scripts, consider
lib/scripts/*as folder.
Keeping tasks slim
For readability and testing it's easier to keep your tasks slim. We suggest to use folders inside the tasks or scripts folder.
Example for a task:
The slim task lib/tasks/gitlab.rb:
Automatic Log Rotation in Rails
Rails log files rotate automatically when they reach approx. 100MB:
$ ls -lh log/
-rw-r--r-- 1 user group 55M Sep 15 09:54 development.log
-rw-r--r-- 1 user group 101M Aug 22 13:45 development.log.0
This behavior is a built-in feature of Ruby's standard Logger class, which Rails uses by default.
To control the maximum file size, set config.log_file_size in yo...
Rails: Using normalizes without copying code
Rails 7.1 added the normalizes method which can be used to normalize user input.
It lets you define the fields you want to normalize and how to normalize them. In the example below, the Movie#title attribute is stripped from leading and trailing whitespace automatically:
class Movie < ApplicationRecord
normalizes :title, with: -> { _1.strip }
end
Tip
Normalization lambdas are not called for
nilvalues by default. To normalizenilvalues, pa...
routing-filter is broken with Rails 7.1
If you are using the routing-filter gem in your Rails 7.1 app for managing URL segments for locales or suffixes, you will notice that the filters do no longer apply, routes are broken and the necessary parameters are no longer extracted. That is because routing-filter patches Rails' find_routes-method to get the current path and apply its defined filters on it. These filters then modify the params that are handed over to your controller action. This way you receive a locale parameter from a ...
Rails cache connection settings
If you're using a Redis cache in Rails (e.g. :redis_cache_store), it's possible to configure additional parameters for your Redis connection.
Example config for Rails 7.2
config.cache_store = :redis_cache_store, {
pool: { timeout: 0.5 },
read_timeout: 0.2, # default 1 second
write_timeout: 0.2, # default 1 second
# Attempt two reconnects with some wait time in between
reconnect_attempts: [1, 5], # default `1` attempt in Redis 5+
url: REDIS_URL,
error_handler: ->(method:, returning:, exception:) {
Sentry.captur...
Rails: Overriding view templates under certain conditions only
Rails offers a way to prepend (or append) view paths for the current request. This way, you can make the application use different view templates for just that request.
Example
A use case of this is a different set of view templates that should be used under certain circumstances:
class UsersController < ApplicationController
before_action :prepare_views
def index
# ...
end
private
def prepare_views
if <condition>
prepend_view_path Rails.root.join('app', 'views', 'special')
end
end
...
Logging multiple lines in Rails without making filtering your logs difficult
Rails' default logger prefixes each log entry with timestamp and tags (like request ID).
For multi-line entries, only the first line is prefixed which can give you a hard time when grepping logs.
Example
Rails.logger.info(<<~TEXT)
Response from example.com:
Status: 200
Body: It works!
TEXT
With that, the following is written to your log file.
I, [2024-10-04T08:12:16.576463 #1917250] INFO -- : [97e45eae-a220-412d-96ad-e9e148ead71d] Response from example.com:
Status: 200
Body: It works!
If you then run `grep...
Upgrading a Rails app to Cucumber 3
Upgrade gems
You need to update a lof gems. Make sure you don't have any version constraints in your Gemfile or your bundle update won't do anything!
Upgrade cucumber_priority:
bundle update cucumber_priority
Upgrade spreewald:
bundle update spreewald
Upgrade cucumber_factory:
bundle update cucumber_factory
Upgrade parallel_tests:
bundle update parallel_tests
Even on the latest version, parallel_tests will print some deprecation warnings due to using an older formatter A...
Rails: Example on how to extract domain independent code from the `app/models` folder to the `lib/` folder
This cards describes an example with a Github Client on how to keep your Rails application more maintainable by extracting domain independent code from the app/models folder to the lib/ folder. The approach is applicable to arbitrary scenarios and not limited to API clients.
Example
Let's say we have a Rails application that synchronizes its users with the Github API:
.
└── app
└── models
├── user
│ ├── github_client.rb
│ └── sychronizer.rb
└── user.rb
In this example the app folder ...
Rails: Destroying vs Deleting
Rails offers several ways to remove records. They differ in whether they instantiate records, fire callbacks (including dependent: associations) and how they manage relation state afterward.
destroy_all
This is the definition of destroy_all:
# ActiveRecord::Relation
def destroy_all
records.each(&:destroy).tap { reset }
end
-
recordsevaluates SQL, caches result -
.each(&:destroy)iterates the cached Array, returns it -
.tap { reset }callsRelation#reset, clearsrecordsandloaded, returns the Array un...
Rails: Using require and permit for attributes
Raising errors for required and permitted attributes makes it easier to find errors in your application during development and in tests. Consider this approach if you want to strengthen the params handling in your application.
Example
# config/application.rb
config.action_controller.action_on_unpermitted_parameters = :raise
def user_params
params.require(:user).permit(:full_name)
end
Effects
- This raises an error `Ac...
Rails: Join model table migration template
When creating a database table for a join model without further importance, you can use Rails' create_join_table:
class CreateSchoolsStudents < ActiveRecord::Migration[7.2]
def change
create_join_table :schools, :students, column_options: { foreign_key: true } do |t|
# t.timestamps # Optional
t.index [:student_id, :school_id], unique: true
end
end
end
This will create a table without an id column and without timestamps. It will have school_id and student_id columns with null: false constraints ...
How to disable irb's welcome banner or how to enable it for the Rails console
Version 1.18.0 of irb introduced a welcome banner.
Here is how to disable it, or how to add it to your Rails console.
Welcome banner
Modern versions of irb show a pretty banner which prints
- IRB version,
- Ruby version,
- a random IRB command hint,
- current directory.
How to disable
If you prefer not to see this banner, update your ~/.irbrc (or create, if you don't have one yet) and add:
IRB.conf[:SHOW_BANNER]...
Postgres Ranges
Postgres supports multiple built-in range datatypes:
int4rangeint8rangenumrange-
tsrange(range with timestamp without timezone) -
tstzrange(range with timestamp with timezone) daterange
They represent a start and endpoint of something in a single column. Image you're building a vacation booking feature:
create_table :events do |t|
t.date :starts_on
t.date :ends_on
end
This is how you would use a range type in a migration:
create_table :vacations do |t|
t.daterange :period
end
See [t...
Rails: namespacing models with table_name_prefix instead of table_name
When you want to group rails models of a logical context, namespaces are your friend. However, if you have a lot of classes in the same namespace it might be tedious to specify the table name for each class seperately:
class Accounting::Invoice < ApplicationRecord
self.table_name = 'accounting_invoices'
...
end
class Accounting::Payment < ApplicationRecord
self.table_name = 'accounting_payments'
...
end
A replacement for the self.table_name-assignment is the table_name_prefix in the module definition:
modu...
Rails: Do not load frameworks you don't need
Rails is split into a large number of (sub-) frameworks.
The most important and central of those are
- activesupport (extends the Ruby standard library)
- activerecord / activemodel (ORM for Rails)
- actionview / actionpack (controller / views)
- actionmailer (sends mails)
However, there are also some more situational frameworks included, such as
- actioncable (real time communications using websockets)
- actionmailbox (receives mails)
- actiontext (support for WYSIWYG text editor)
- activejob (background jobs)
- activestorage (file uplo...
Rails: Your index actions probably want strict_loading
By activating strict_loading you force developers to address n+1 queries by preloading all associations used in the index view. Using an association that is not preloaded will raise an ActiveRecord::StrictLoadingViolationError.
I think it's a good default to activate strict_loading in your controllers' #index actions. This way, when a change introduces an n+1 query, you...
Upgrade Rails: Awareness list
Disclaimer
This card is a collection of guides and things to have in mind when upgrading to a specific version. It is not meant to be complete, so please feel free to contribute!
General workflows
Upgrade to Rails 7
- Don't use log level :debug in your production environments
- [Rails 7.1: Take care of...
Rails: Comparison of assignable_values and Active Record enum types
We are using assignable_values for managing enum values in Rails. Nevertheless Rails is adding more support for enum attributes, allowing to have a closer look at the current feature set in comparison to our still preferred option assignable_values.
Active Record enum attribute interface
By default Rails is mapping enum attributes to integers:
...
Checklist: Rails Authentication
Authentication is a special part of web applications. On the one hand, it usually is a crucial security mechanism restrict access to certain people and roles. On the other hand, most users authenticate only once, so it is very unlikely to spot issues by accident.
So, here comes a quick checklist to help you verifying your authentication solution is all set.
- This should be default: use HTTPS with HSTS. The HSTS part is important.
- Use a reliable authentication solution, e.g. [Compose Rails authentication primitives](https://makandracards...
Rails 3: Sending tempfiles for download
When you create a temporary file (e.g. to store a generated Excel sheet) and try to send it to the browser from a controller, it won't work by default. Take this controller action:
class FoosController < ApplicationController
def download
file = Tempfile.new('foo')
file.puts 'foo'
file.close
send_file file.path
end
end
Accessing this controller action will usually raise a 404 not found in the browser and the Apache log will say:
The given path was above the root path: xsendfile: unable to find file: /tm...
How to keep using secrets.yml after upgrading to Rails 7.2
Rails 5.2 soft-deprecated the storage of secrets in secrets.yml in favor of a new thing, credentials.yml.enc. Rails 7.1 deprecated secrets and Rails 7.2 finally removed it.
In our permissions model, it does not matter much whether secrets or credentials are used. While we'll use credentials in new applications (for conformity), for existing applications it may be appropriate to keep using secrets.yml.
Restoring secrets in Rails 7.2+
Restoring `Rails.applic...
Searchkick: async reindexing fails for rails 7 with redis 4
After an upgrade to rails 7 I noticed that async reindexing jobs of Searchkick were failing for Model.reindex(mode: :async, wait: true):
/home/a_user/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/searchkick-5.3.1/lib/searchkick/relation_indexer.rb:142:in `block in batch_job': undefined method `call' for nil (NoMethodError)
Searchkick.with_redis { |r| r.call("SADD", batches_key, [batch_id]) }
^^^^^
from /home/a_user/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/searchkick-5.3.1/lib/searchkick.r...