Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* [#2960](https://github.com/ruby-grape/grape/pull/2960): Lint every response of an API once under `lint!`, including the router's 404 and mounted Rack apps, instead of inside each endpoint's stack - [@ericproulx](https://github.com/ericproulx).
* [#2966](https://github.com/ruby-grape/grape/pull/2966): Require Ruby 3.3.1, the first 3.3 release that parses anonymous argument forwarding from inside a block - [@ericproulx](https://github.com/ericproulx).
* [#3008](https://github.com/ruby-grape/grape/pull/3008): Add the grape-on-rack app as a live-server memory profiling harness - [@ericproulx](https://github.com/ericproulx).
* [#3022](https://github.com/ruby-grape/grape/pull/3022): Compile the APIs mounted in a Rails application's routes at boot when it eager loads - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

#### Fixes
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ By default Grape will compile the routes on the first request, but it is possibl
Twitter::API.compile!
```

This can be added to your `config.ru` (if using rackup), `application.rb` (if using rails), or any file that loads your server.
This can be added to your `config.ru` (if using rackup) or any file that loads your server. In Rails, the APIs mounted in the routes are compiled for you, see [Rails](#rails).

### Rack

Expand Down Expand Up @@ -197,6 +197,15 @@ Modify `config/routes`:
```ruby
mount Twitter::API => '/'
```

Whenever `config.eager_load` is on, as it is in production, Rails compiles the APIs mounted in its routes at boot, including those an engine's routes mount. An API that reaches Rails another way, such as inside a `Rack::Builder`, is compiled on its first request. To compile it at boot, call `compile!` once the application has initialized, since the API cannot be autoloaded yet in `config/application.rb` or an initializer:

```ruby
config.after_initialize do
Twitter::API.compile!
end
```

#### Zeitwerk
Rails's default autoloader is `Zeitwerk`. By default, it inflects `api` as `Api` instead of `API`. To make our example work, you need to uncomment the lines at the bottom of `config/initializers/inflections.rb`, and add `API` as an acronym:

Expand Down
6 changes: 6 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ Upgrading Grape

### Upgrading to >= 4.1.0

#### APIs mounted in a Rails application's routes are compiled at boot

When `config.eager_load` is on, as it is in production, Grape's Railtie now compiles the APIs mounted in the application's routes, and in the routes of the engines it mounts, as soon as the routes are loaded, rather than leaving it to the first request each process answers ([#3022](https://github.com/ruby-grape/grape/pull/3022)). A `compile!` call the application already makes can stay: an API is compiled once.

Compiling builds each endpoint's middleware stack, so a middleware's `initialize` now runs at boot, and before the workers fork under a server that preloads the application, such as Puma with `preload_app!`. A middleware that opens a connection or starts a thread in `initialize` should do it on its first request instead. An API that fails to compile now fails the boot rather than its first request.

#### `logger` applies to the API it is set on

`logger` kept the logger in settings shared by the whole process, so setting it on one API replaced it on every other: two APIs that each set a logger both logged to whichever was set last.
Expand Down
21 changes: 21 additions & 0 deletions lib/grape/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,26 @@ class Railtie < ::Rails::Railtie
initializer 'grape.deprecator' do |app|
app.deprecators[:grape] = Grape.deprecator
end

# Rails builds its own routes' router at boot when it eager loads; compile
# the APIs mounted in them then too, rather than on the first request each
# process answers.
config.after_routes_loaded do |app|
compile_mounted_apis(app.routes) if app.config.eager_load
end

class << self
private

def compile_mounted_apis(route_set)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should this live in route_set, route_set.compile!?

route_set.routes.each do |route|
endpoint = route.app
next compile_mounted_apis(endpoint.rack_app.routes) if endpoint.engine?

rack_app = endpoint.rack_app
rack_app.compile! if rack_app.is_a?(Class) && rack_app < Grape::API
end
end
end
end
end
79 changes: 79 additions & 0 deletions spec/integration/rails/railtie_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,84 @@

it { is_expected.to be(Grape.deprecator) }
end

describe 'compiling the APIs mounted in the routes' do
let(:api) { Class.new(Grape::API) { get('/ping') { 'pong' } } }
let(:mounted) { api }
let(:eager_load) { true }

let(:test_app) do
require 'action_controller/railtie'
# Every application shares config.action_view, and load_defaults below
# writes settings back into it that ActionView::Base has no setter for.
# Loading ActionView::Base first runs the :action_view hooks an earlier
# example's application left pending, before they can read them;
# eager loading would otherwise load it in the middle of the boot.
require 'action_view/base'

# https://github.com/rails/rails/issues/51784
# same error as described if not redefining the following
ActiveSupport::Dependencies.autoload_paths = []
ActiveSupport::Dependencies.autoload_once_paths = []

eager = eager_load
app = mounted
Class.new(Rails::Application) do
config.eager_load = eager
config.load_defaults "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}"
routes.append { mount app => '/' }
end
end

# Eager loading calls I18n.eager_load!, after which the backend reloads its
# translations at once on every I18n.reload!, under whatever
# available_locales a later example has set, instead of on the next lookup.
around do |example|
backend = I18n.backend
I18n.backend = I18n::Backend::Simple.new
example.run
ensure
I18n.backend = backend
end

before do
stub_const('GrapeApi', api)
# Rails 8 hands after_routes_loaded Rails.application, which is memoized
# to the first application this process initialized.
allow(Rails).to receive(:application).and_return(test_app.instance)
end

context 'when Rails eager loads' do
it 'compiles them at boot' do
expect(api).to receive(:compile!).and_call_original
test_app.initialize!
end
end

context 'when Rails does not eager load' do
let(:eager_load) { false }

it 'leaves them to the first request' do
expect(api).not_to receive(:compile!)
test_app.initialize!
end
end

context 'when an engine mounts the API' do
let(:engine) { Class.new(Rails::Engine) }
let(:mounted) { engine }

before do
stub_const('GrapeEngine', engine)
mounted_api = api
engine.routes.append { mount mounted_api => '/' }
end

it 'compiles it at boot' do
expect(api).to receive(:compile!).and_call_original
test_app.initialize!
end
end
end
end
end
Loading