Skip to content

Commit

Permalink
Document how to reload at boot time [ci skip]
Browse files Browse the repository at this point in the history
Co-authored-by: Haroon Ahmed <haroon.ahmed25@gmail.com>
  • Loading branch information
fxn and hahmed committed May 17, 2020
1 parent 683d9d5 commit 524d678
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 8 deletions.
39 changes: 31 additions & 8 deletions guides/source/autoloading_and_reloading_constants.md
Expand Up @@ -131,7 +131,7 @@ Rails detects files have changed using an evented file monitor (default), or wal

In a Rails console there is no file watcher active regardless of the value of `config.cache_classes`. This is so because, normally, it would be confusing to have code reloaded in the middle of a console session, the same way you generally want an individual request to be served by a consistent, non-changing set of application classes and modules.

However, you can force a reload in the console executing `reload!`:
However, you can force a reload in the console by executing `reload!`:

```bash
$ bin/rails c
Expand All @@ -151,9 +151,9 @@ as you can see, the class object stored in the `User` constant is different afte

It is very important to understand that Ruby does not have a way to truly reload classes and modules in memory, and have that reflected everywhere they are already used. Technically, "unloading" the `User` class means removing the `User` constant via `Object.send(:remove_const, "User")`.

Therefore, if you store a reloadable class or module object in a place that is not reloaded, that value is going to become stale.
Therefore, code that references a reloadable class or module, but that is not executed again on reload, becomes stale. Let's see an example next.

For example, if an initializer stores and caches a certain class object
Let's consider this initializer:

```ruby
# config/initializers/configure_payment_gateway.rb
Expand All @@ -162,18 +162,27 @@ $PAYMENT_GATEWAY = Rails.env.production? ? RealGateway : MockedGateway
# DO NOT DO THIS.
```

and `MockedGateway` gets reloaded, `$PAYMENT_GATEWAY` still stores the class object `MockedGateway` evaluated to when the initializer ran. Reloading does not change the class object stored in `$PAYMENT_GATEWAY`.
The idea would be to use `$PAYMENT_GATEWAY` in the code, and let the initializer set that to the actual implementation dependending on the environment.

Similarly, in the Rails console, if you have a user instance and reload:
On reload, `MockedGateway` is reloaded, but `$PAYMENT_GATEWAY` is not updated because initializers only run on boot. Therefore, it won't reflect the changes.

There are several ways to do this safely. For instance, the application could define a class method `PaymentGateway.impl` whose definition depends on the environment; or could define `PaymentGateway` to have a parent class or mixin that depends on the environment; or use the same global variable trick, but in a reloader callback, as explained below.

Let's see other situations that involve stale class or module objects.

Check this Rails console session:

```
> user = User.new
> joe = User.new
> reload!
> alice = User.new
> joe.class == alice.class
false
```

the `user` object is an instance of a stale class object. Ruby gives you a new class if you evaluate `User` again, but does not update the class `user` is an instance of.
`joe` is an instance of the original `User` class. When there is a reload, the `User` constant evaluates to a different, reloaded class. `alice` is an instance of the current one, but `joe` is not, his class is stale. You may define `joe` again, start an IRB subsession, or just launch a new console instead of calling `reload!`.

Another use case of this gotcha is subclassing reloadable classes in a place that is not reloaded:
Another situation in which you may find this gotcha is subclassing reloadable classes in a place that is not reloaded:

```ruby
# lib/vip_user.rb
Expand All @@ -185,6 +194,20 @@ if `User` is reloaded, since `VipUser` is not, the superclass of `VipUser` is th

Bottom line: **do not cache reloadable classes or modules**.

### Autoloading when the application boots

Applications can safely autoload constants during boot using a reloader callback:

```
Rails.application.reloader.to_prepare do
$PAYMENT_GATEWAY = Rails.env.production? ? RealGateway : MockedGateway
end
```

That block runs when the application boots, and every time code is reloaded.

NOTE: For historical reasons, this callback may run twice. The code it executes must be idempotent.


Eager Loading
-------------
Expand Down
5 changes: 5 additions & 0 deletions railties/CHANGELOG.md
@@ -1,3 +1,8 @@
* The autoloading guide for Zeitwerk mode documents how to autoload classes
during application boot in a safe way.

*Haroon Ahmed*, *Xavier Noria*

* Use explicit `config/boot_with_spring.rb` boot file for bin/rails and bin/rake, which allows us to restrict Spring loading
to only test and development, and everywhere to be able to skip spring by passing UNSPRUNG=1 as an env variable.

Expand Down

0 comments on commit 524d678

Please sign in to comment.