Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid shelling out for generator generate action #37516

Merged

Conversation

jonathanhefner
Copy link
Member

@jonathanhefner jonathanhefner commented Oct 20, 2019

This change addresses a few issues:

First, shelling out to the rails command requires the destination directory to contain certain files that the command uses to initialize itself. While this is not an issue when running generators normally, it is troublesome when testing generator-invoking generators which output to ephemeral destination directories.

Second, shelling out to the rails command is very slow. This also is not a particular concern when running generators normally, but it makes test suites for generator-invoking generators painfully slow.

Third, shelling out to the rails command fails silently by default. Such silent failures can be surprising, and can lead to confusing downstream failures.


Some previous history regarding the generate action:

The changes in this PR are similar to #34418, but with backward-compatibility concerns mostly addressed. However, unlike the alternative #34420, generate will now always raise on failure. This could be made opt-in behavior, but I could not think of a scenario where you would want generate to fail silently.

This PR does not revert #34420, which affected more than generate, but this PR does obsolete the abort_on_failure option for generate. Since abort_on_failure was previously only tested with generate, this PR adds tests for the abort_on_failure option with other actions.

This PR does revert #34980, which is no longer necessary. (See #37979)

Also, a crude and informal performance measurement: for a test suite with fewer than a dozen generator-invoking-generator tests, this change reduced the run time from 57 seconds to 3.3 seconds. 馃殌

@rails-bot rails-bot bot added the railties label Oct 20, 2019
@jonathanhefner
Copy link
Member Author

The build errors are coming from Active Job. I will take a closer look tomorrow.

@jonathanhefner
Copy link
Member Author

While digging into the Active Job errors, I realized a use case which does require that generate shell out. Imagine two consecutive generate calls where the 1st call generates app config code that the 2nd depends on. Without shelling out, Rails.application is not "reloaded" for the 2nd call, and thus the generated app config code will not take effect.

The benefits of not shelling out are still very desirable, though. My current thinking is to at least add a :spawn option to generate (or possibly to rails_command). It could default to true, but when set to false, the action would use Rails::Command.invoke instead of execute_command :rails.

Thoughts, anyone?

@deivid-rodriguez
Copy link
Contributor

Third, shelling out to the rails command fails silently by default. Such silent failures can be surprising, and can lead to confusing downstream failures.

I think this third issue would be solved by rails/thor#651?

Copy link
Contributor

@kaspth kaspth left a comment

Choose a reason for hiding this comment

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

鉂わ笍

railties/lib/rails/generators/actions.rb Show resolved Hide resolved
@@ -288,14 +292,12 @@ def log(*args) # :doc:
def execute_command(executor, command, options = {}) # :doc:
log executor, command
env = options[:env] || ENV["RAILS_ENV"] || "development"
rails_env = " RAILS_ENV=#{env}" unless options[:without_rails_env]
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we inlining this?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure I understand the question. Are you referring to the concerns you brought up in #34980 (comment)?

railties/test/generators/actions_test.rb Show resolved Hide resolved
railties/lib/rails/generators/actions.rb Outdated Show resolved Hide resolved
@kaspth
Copy link
Contributor

kaspth commented Nov 10, 2019

While digging into the Active Job errors, I realized a use case which does require that generate shell out. Imagine two consecutive generate calls where the 1st call generates app config code that the 2nd depends on. Without shelling out, Rails.application is not "reloaded" for the 2nd call, and thus the generated app config code will not take effect.

Is there a way to make Rails.application reload itself? Or what's the stuff that isn't reloaded properly? Do we just not have the appropriate reloaders in place to pick up e.g. app/models/post.rb was in app/models because app/models was created simultaneously?

@jonathanhefner jonathanhefner force-pushed the generator-generate-avoid-shell-out branch 3 times, most recently from 05c1142 to 233c04b Compare November 13, 2019 20:33
@jonathanhefner
Copy link
Member Author

Build errors are now unrelated. (They've been happening since ace8d6d, though I suspect the cause is some upstream dependency.)

@jonathanhefner
Copy link
Member Author

@kaspth

Is there a way to make Rails.application reload itself? Or what's the stuff that isn't reloaded properly? Do we just not have the appropriate reloaders in place to pick up e.g. app/models/post.rb was in app/models because app/models was created simultaneously?

There does not seem to be a way to have Rails reload app config code, e.g. config/application.rb. So (quite) unfortunately this new behavior may have to be opt-in after all, to preserve backward compatibility.

My current approach is to add an :inline option to rails_command, which generate now delegates to. When inline: true, rails_command will call Rails::Command.invoke directly in the current process, instead of shelling out.

@jonathanhefner
Copy link
Member Author

@deivid-rodriguez

I think this third issue would be solved by erikhuda/thor#651?

If I understand that PR correctly, exit_on_failure? will default to false, so shelling out would still fail silently by default. It would be up to each generator class to override exit_on_failure? and return true, correct?

However, I did add a change to this PR so that generate always sets abort_on_failure: true. So now calls to generate will always raise on failure. What do you think?

@jonathanhefner jonathanhefner force-pushed the generator-generate-avoid-shell-out branch from 233c04b to bae33d6 Compare November 16, 2019 18:53
@deivid-rodriguez
Copy link
Contributor

I'm not sure. Rails does this:

module Rails
module Generators
class AppGenerator # :nodoc:
# We want to exit on failure to be kind to other libraries
# This is only when accessing via CLI
def self.exit_on_failure?
true
end
end
end

It would seem to me that the expected behaviour is to actually not raise in some cases, but in others ("when accessing via CLI"), the thor PR would fix the issue?

@jonathanhefner
Copy link
Member Author

@deivid-rodriguez I think rails/thor#651 is intended to address the case where run is never allowed to fail, inside a single generator class (and its subclasses). But the case I'm trying to address is, more specifically, generate is never allowed to fail, for all generator classes.

This change addresses a few issues:

First, shelling out to the `rails` command requires the destination
directory to contain certain files that the command uses to initialize
itself.  While this is not an issue when running generators normally, it
is troublesome when testing generator-invoking generators which output
to ephemeral destination directories.

Second, shelling out to the `rails` command is very slow.  This also is
not a particular concern when running generators normally, but it makes
test suites for generator-invoking generators painfully slow.

Third, shelling out to the `rails` command fails silently by default.
Such silent failures can be surprising, and can lead to confusing
downstream failures.
@jonathanhefner jonathanhefner force-pushed the generator-generate-avoid-shell-out branch from bae33d6 to dcc3c85 Compare January 9, 2020 21:50
@kaspth kaspth merged commit e807099 into rails:master Feb 9, 2020
@kaspth
Copy link
Contributor

kaspth commented Feb 9, 2020

Perfect @jonathanhefner, thanks for preserving backwardscompatibilty too. What's the next step to get this speed boost? Is it mostly for our own generator tests?

jonathanhefner added a commit to jonathanhefner/rails that referenced this pull request Feb 10, 2020
@jonathanhefner
Copy link
Member Author

@kaspth Any generator which invokes the rails_command or generate actions can opt in to the speed boost with inline: true. Looking through the "meta-generators" in Rails core, e.g. ScaffoldGenerator and ControllerGenerator, all of them appear to use hook_for rather than generate. However, there are a few generators which use rails_command. I've created #38429 to add inline: true to those, but I've had difficulty judging the performance improvement (see my comments there).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants