├── .github └── workflows │ └── main.yml ├── .gitignore ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── routes_lazy_routes.rb └── routes_lazy_routes │ ├── application.rb │ ├── lazy_routes_middleware.rb │ ├── railtie.rb │ ├── routes_reloader_wrapper.rb │ ├── tasks │ └── routes_lazy_routes.rake │ └── version.rb ├── routes_lazy_routes.gemspec └── test ├── Rakefile ├── bin └── rails ├── config ├── application.rb ├── boot.rb └── routes.rb ├── rake_task_test.rb ├── routes_lazy_routes_test.rb └── test_helper.rb /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | ruby_version: [ruby-head, '3.3', '3.2', '3.1'] 10 | rails_version: ['edge', '7.1', '7.0', '6.1'] 11 | 12 | include: 13 | - ruby_version: '3.0' 14 | rails_version: '7.1' 15 | - ruby_version: '3.0' 16 | rails_version: '7.0' 17 | - ruby_version: '3.0' 18 | rails_version: '6.1' 19 | - ruby_version: '3.0' 20 | rails_version: '6.0' 21 | 22 | - ruby_version: '2.7' 23 | rails_version: '7.1' 24 | - ruby_version: '2.7' 25 | rails_version: '7.0' 26 | - ruby_version: '2.7' 27 | rails_version: '6.1' 28 | - ruby_version: '2.7' 29 | rails_version: '6.0' 30 | - ruby_version: '2.7' 31 | rails_version: '5.2' 32 | 33 | - ruby_version: '2.6' 34 | rails_version: '6.0' 35 | - ruby_version: '2.6' 36 | rails_version: '5.2' 37 | 38 | runs-on: ubuntu-latest 39 | 40 | env: 41 | RAILS_VERSION: ${{ matrix.rails_version }} 42 | 43 | steps: 44 | - uses: actions/checkout@v3 45 | 46 | - uses: ruby/setup-ruby@v1 47 | with: 48 | ruby-version: ${{ matrix.ruby_version }} 49 | rubygems: latest 50 | bundler-cache: true 51 | 52 | - run: bundle exec rake 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | .byebug_history 11 | /test/log/ 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in routes_lazy_routes.gemspec 6 | gemspec 7 | 8 | if ENV['RAILS_VERSION'] == 'edge' 9 | gem 'rails', git: 'https://github.com/rails/rails.git' 10 | elsif ENV['RAILS_VERSION'] 11 | gem 'rails', "~> #{ENV['RAILS_VERSION']}.0" 12 | end 13 | 14 | gem "rake", "~> 13.0" 15 | 16 | gem "minitest", "~> 5.0" 17 | 18 | gem 'base64' 19 | gem 'bigdecimal' 20 | gem 'mutex_m' 21 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Akira Matsuda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # routes_lazy_routes 2 | 3 | routes_lazy_routes is an evil Rails plugin that defers loading the whole bloody routes until the server gets the first request, so the app can spin up quickly. 🤘 4 | 5 | This voodoo gem is designed especially for you who are maintaining a huge legacy Rails app that contains hundreds of routes that forces you to wait dozens of seconds per every `rails` command invocation. 6 | 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile: 11 | 12 | ```ruby 13 | gem 'routes_lazy_routes' 14 | ``` 15 | 16 | If you're not brave enough, you can limit the group not to include `production` environment. 17 | Indeed, enabling this gem for the processes like batch or job may improve their runtime performance, but so far as that's not a critical problem for your production system, the following setup might be safer. 18 | 19 | ```ruby 20 | group :development, :test do 21 | gem 'routes_lazy_routes' 22 | end 23 | ``` 24 | 25 | 26 | ## Usage 27 | 28 | You have nothing to do with it. It should just work beneath the skin. 29 | 30 | 31 | ## Trade-off 32 | 33 | The first visitor of the server should sacrifice their time for the Rails process to load the routes. First strike is deadly. 34 | If you're bundling this in the production server, it'd be a good idea to throw a jab to the server right after the deployment in order to warm up before accepting real client requests. 35 | 36 | 37 | ## Notes 38 | 39 | - You can manually eager_load the routes by calling `RoutesLazyRoutes.eager_load!` (the "load runner"). 40 | 41 | - `Rails.application.eager_load!` automatically invokes `RoutesLazyRoutes.eager_load!` since that should be what we expect for `Rails.application.eager_load!`. 42 | 43 | - Loading an integration test automatically kicks `RoutesLazyRoutes.eager_load!` since AD::Integration expects the routes to be loaded. 44 | 45 | - And, as already explained, sending a request to the Rails server automatically runs `RoutesLazyRoutes.eager_load!` on the server. 46 | 47 | - On the other hand, you need manually calling `RoutesLazyRoutes.eager_load!` inside your worker process (e.g. Sidekiq) to resolve named routes like the following: 48 | ``` ruby 49 | # config/initializers/sidekiq.rb 50 | Sidekiq.configure_server do |config| 51 | if defined?(RoutesLazyRoutes) 52 | Rails.application.config.after_initialize do 53 | RoutesLazyRoutes.eager_load! 54 | end 55 | end 56 | end 57 | ``` 58 | 59 | ## Contributing 60 | 61 | Patches are welcome on GitHub at https://github.com/amatsuda/routes_lazy_routes. 62 | 63 | 64 | ## License 65 | 66 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 67 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | 6 | Rake::TestTask.new(:test) do |t| 7 | t.libs << "test" 8 | t.libs << "lib" 9 | t.test_files = FileList["test/**/*_test.rb"] 10 | end 11 | 12 | task default: :test 13 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "routes_lazy_routes" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/routes_lazy_routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'routes_lazy_routes/version' 4 | require_relative 'routes_lazy_routes/lazy_routes_middleware' 5 | require_relative 'routes_lazy_routes/routes_reloader_wrapper' 6 | require_relative 'routes_lazy_routes/railtie' 7 | 8 | module RoutesLazyRoutes 9 | class << self 10 | # The root of evil 11 | def arise! 12 | Rails::Application::RoutesReloader.class_eval do 13 | class << self 14 | def new 15 | RoutesLazyRoutes::RoutesReloaderWrapper.new super 16 | end 17 | end 18 | end 19 | end 20 | 21 | # The load runner 22 | def eager_load! 23 | if RoutesLazyRoutes::RoutesReloaderWrapper === (reloader = Rails.application.routes_reloader) 24 | reloader.reload! 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/routes_lazy_routes/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RoutesLazyRoutes 4 | module Application 5 | module LoadRunner 6 | # We expect `Rails.application.eager_load!` to load all routes as well 7 | def eager_load! 8 | RoutesLazyRoutes.eager_load! 9 | 10 | super 11 | end 12 | end 13 | 14 | module TaskLoader 15 | # A monkey-patch that loads our Rake task for enhancing `rake routes` after Rails loads all other tasks. 16 | # Just declaring our own `rake_tasks` in the railtie cannot achieve this, since calling each railtie's `rake_tasks` is done before requiring "rails/tasks", 17 | # so enhancing Rails' Rake task from a gem this way seems impossible. 18 | def load_tasks(*) 19 | super 20 | 21 | load "#{__dir__}/tasks/routes_lazy_routes.rake" 22 | end 23 | end 24 | 25 | # A monkey-patch that eager loads routes before the routes command is executed by prepending this module to the 26 | # require_environment! method that's called when boot_application! is executed performing console commands. 27 | module RoutesCommandEagerLoader 28 | def require_environment! 29 | super 30 | 31 | RoutesLazyRoutes.eager_load! 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/routes_lazy_routes/lazy_routes_middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RoutesLazyRoutes 4 | class LazyRoutesMiddleware 5 | def initialize(app) 6 | @app = app 7 | @loaded = false 8 | end 9 | 10 | def call(env) 11 | unless @loaded 12 | RoutesLazyRoutes.eager_load! 13 | @loaded = true 14 | end 15 | 16 | @app.call env 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/routes_lazy_routes/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'application' 4 | 5 | module RoutesLazyRoutes 6 | class Railtie < ::Rails::Railtie 7 | # Extending the following modules have to be done very early, like before executing any initializer, so here it is 8 | Rails::Application.prepend RoutesLazyRoutes::Application::TaskLoader 9 | 10 | if defined? Rails::Command::RoutesCommand 11 | Rails::Application.prepend RoutesLazyRoutes::Application::RoutesCommandEagerLoader 12 | end 13 | 14 | initializer :routes_lazy_routes, before: :add_routing_paths do 15 | RoutesLazyRoutes.arise! 16 | 17 | Rails.application.config.middleware.use LazyRoutesMiddleware 18 | 19 | Rails.application.extend RoutesLazyRoutes::Application::LoadRunner 20 | 21 | ActiveSupport.on_load :action_dispatch_integration_test, run_once: true do 22 | RoutesLazyRoutes.eager_load! 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/routes_lazy_routes/routes_reloader_wrapper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RoutesLazyRoutes 4 | class RoutesReloaderWrapper 5 | delegate :paths, 6 | :eager_load=, 7 | :run_after_load_paths=, 8 | :updated?, 9 | :route_sets, 10 | :external_routes, 11 | to: :@original_routes_reloader 12 | 13 | def initialize(original_routes_reloader) 14 | @original_routes_reloader = original_routes_reloader 15 | @mutex = Mutex.new 16 | end 17 | 18 | def execute 19 | # pretty vacant 20 | end 21 | 22 | def reload! 23 | @mutex.synchronize do 24 | if Rails.application.routes_reloader == self 25 | Rails.application.instance_variable_set :@routes_reloader, @original_routes_reloader 26 | @original_routes_reloader.execute 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/routes_lazy_routes/tasks/routes_lazy_routes.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if Rake::Task.task_defined?('routes') 4 | namespace :routes_lazy_routes do 5 | task :eager_load do 6 | RoutesLazyRoutes.eager_load! 7 | end 8 | end 9 | 10 | Rake::Task['routes'].enhance ['routes_lazy_routes:eager_load'] 11 | end 12 | -------------------------------------------------------------------------------- /lib/routes_lazy_routes/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RoutesLazyRoutes 4 | VERSION = '0.4.3' 5 | end 6 | -------------------------------------------------------------------------------- /routes_lazy_routes.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/routes_lazy_routes/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "routes_lazy_routes" 7 | spec.version = RoutesLazyRoutes::VERSION 8 | spec.authors = ["Akira Matsuda"] 9 | spec.email = ["ronnie@dio.jp"] 10 | 11 | spec.summary = 'A Rails routes lazy loader' 12 | spec.description = 'A Rails plugin that defers loading the whole bloody routes so the app can spin up quickly' 13 | spec.homepage = 'https://github.com/amatsuda/routes_lazy_routes' 14 | spec.license = "MIT" 15 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") 16 | 17 | spec.metadata["homepage_uri"] = spec.homepage 18 | spec.metadata["source_code_uri"] = 'https://github.com/amatsuda/routes_lazy_routes' 19 | # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." 20 | 21 | # Specify which files should be added to the gem when it is released. 22 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 23 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 24 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } 25 | end 26 | spec.require_paths = ["lib"] 27 | 28 | spec.add_dependency 'railties' 29 | spec.add_dependency 'actionpack' 30 | spec.add_development_dependency 'byebug' 31 | end 32 | -------------------------------------------------------------------------------- /test/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /test/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RoutesLazyRoutesTestApp 4 | Application = Class.new(Rails::Application) do 5 | config.eager_load = false 6 | config.active_support.deprecation = :log 7 | config.root = File.expand_path('..', __dir__) 8 | config.secret_key_base = 'Routes, lazy routes.' * 4 9 | end 10 | end 11 | 12 | RoutesLazyRoutesTestApp::Application.initialize! 13 | -------------------------------------------------------------------------------- /test/config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __dir__) 4 | 5 | require "bundler/setup" 6 | 7 | require "rails" 8 | require "action_controller/railtie" 9 | 10 | # Bundler.require(*Rails.groups) 11 | -------------------------------------------------------------------------------- /test/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | get 'foo', to: ->(_env) { [200, {}, ['hello']] } 5 | end 6 | -------------------------------------------------------------------------------- /test/rake_task_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class RakeTaskTest < Minitest::Test 6 | def test_that_rake_task_properly_loads 7 | # Just testing that loading the rake task doesn't raise any error 8 | Rails.application.load_tasks 9 | end 10 | 11 | def test_rails_routes_command_output 12 | rails_version = ENV['RAILS_VERSION'] || "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" 13 | routes_output = `cd test && RAILS_VERSION=#{rails_version} bin/rails routes` 14 | assert_match /^ *foo GET/, routes_output 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/routes_lazy_routes_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class RoutesLazyRoutesTest < Minitest::Test 6 | def test_it_lazily_loads_the_routes 7 | assert_equal 0, Rails.application.routes.routes.length 8 | 9 | RoutesLazyRoutes.eager_load! 10 | 11 | assert_equal 1, Rails.application.routes.routes.length 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 4 | require 'rails' 5 | require 'byebug' 6 | require 'action_controller' 7 | require "routes_lazy_routes" 8 | 9 | require "minitest/autorun" 10 | 11 | ENV['RAILS_ENV'] ||= 'test' 12 | 13 | require_relative 'config/application' 14 | --------------------------------------------------------------------------------