Skip to content

config.autoload_once_paths is not walked, so constants defined there are unresolved #642

Description

@iberianpig

roundhouse finds support roots in config/application.rb by scanning for config.autoload_paths and config.eager_load_paths (src/ingest/app.rs:5613), and autoload_lib(ignore:) removes directories from them. It does not read config.autoload_once_paths. Rails loads those directories with its once autoloader, so their constants resolve in the app. roundhouse never reads the files and reports every reference to their constants as an error.

Repro

# config/application.rb
config.autoload_lib(ignore: %w[assets tasks autoload_once])
config.autoload_once_paths << "#{root}/lib/autoload_once"

# lib/autoload_once/gateway.rb
module Gateway
  def self.ping = "pong"
end

# app/models/widget.rb
class Widget < ApplicationRecord
  def ping = Gateway.ping
end

In Rails, Rails.autoloaders.once.dirs includes lib/autoload_once and Widget.new.ping returns "pong".

roundhouse check --continue . in the app root, main a3acfb4 (the absolute path of the temporary app is shortened to ./):

./app/models/widget.rb:2:14: error[unsupported]: constant not supported (all targets): Gateway

roundhouse-check: 2 gems: 1 framework, 1 infrastructure, 0 unknown
roundhouse-check: . — 0 parse error(s), 1 error(s), 0 warning(s), 0 gap-attributed note(s), 0 survey gap(s)

The release 2026.9.18 reports the same line as error[send_dispatch_failed]: no known method `ping` on Gateway.

Self-contained repro.rb (bundler/inline; exits 1 while the bug is present)

ruby repro.rb needs network access for bundler/inline and the roundhouse binary (on PATH, in $ROUNDHOUSE, or at /tmp/roundhouse-bin/roundhouse). It writes the app into a temporary directory, boots it in Rails and calls Widget#ping, generates Gemfile.lock with bundle lock --local, then runs roundhouse check --continue . on the same files.

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"
  gem "rails", "~> 8.0"
  gem "sqlite3"
end

require "fileutils"
require "tmpdir"

APP = {
  "Gemfile" => <<~RUBY,
    source "https://rubygems.org"
    gem "rails", "~> 8.0"
    gem "sqlite3"
  RUBY
  "config/application.rb" => <<~RUBY,
    require "rails/all"

    module Repro
      class Application < Rails::Application
        config.load_defaults 8.0
        config.autoload_lib(ignore: %w[assets tasks autoload_once])
        config.autoload_once_paths << "\#{root}/lib/autoload_once"
      end
    end
  RUBY
  "config/routes.rb" => <<~RUBY,
    Rails.application.routes.draw do
    end
  RUBY
  "db/schema.rb" => <<~RUBY,
    ActiveRecord::Schema[8.0].define(version: 1) do
      create_table "widgets", force: :cascade do |t|
        t.string "name", null: false
        t.timestamps
      end
    end
  RUBY
  "lib/autoload_once/gateway.rb" => <<~RUBY,
    module Gateway
      def self.ping = "pong"
    end
  RUBY
  "app/models/application_record.rb" => <<~RUBY,
    class ApplicationRecord < ActiveRecord::Base
      primary_abstract_class
    end
  RUBY
  "app/models/widget.rb" => <<~RUBY
    class Widget < ApplicationRecord
      def ping = Gateway.ping
    end
  RUBY
}

root = Dir.mktmpdir("roundhouse-repro")
at_exit { FileUtils.remove_entry(root) }
APP.each do |path, source|
  FileUtils.mkdir_p(File.dirname(File.join(root, path)))
  File.write(File.join(root, path), source)
end

puts "== Rails #{Rails.version} runs the app"
ENV["RAILS_ENV"] = "test"
ENV["DATABASE_URL"] = "sqlite3::memory:"
Dir.chdir(root) { require "./config/application" }
Rails.application.config.eager_load = false
Rails.application.initialize!
ActiveRecord::Schema.verbose = false
load File.join(root, "db/schema.rb")

once_dirs = Rails.autoloaders.once.dirs.filter_map { _1.dup.delete_prefix!("#{File.realpath(root)}/") }
puts "Rails.autoloaders.once.dirs under the app root -> #{once_dirs.inspect}"
puts "Widget.new.ping -> #{Widget.new.ping.inspect}"
raise "unexpected result" unless once_dirs.include?("lib/autoload_once") && Widget.new.ping == "pong"

puts "== roundhouse check --continue"
Bundler.with_unbundled_env { system("bundle", "lock", "--local", chdir: root, out: File::NULL, exception: true) }
roundhouse = ENV["ROUNDHOUSE"] ||
  ENV["PATH"].split(File::PATH_SEPARATOR).map { File.join(_1, "roundhouse") }.find { File.executable?(_1) } ||
  "/tmp/roundhouse-bin/roundhouse"
output = IO.popen([roundhouse, "check", "--continue", "."], chdir: root, err: [:child, :out], &:read)
puts output
exit 1 if output.include?("no known method `ping` on Gateway") ||
  output.include?("constant not supported (all targets): Gateway")

Impact

In a Rails 8.0 app with a separate daemon, lib/autoload_once holds the client and protocol code that the app shares with the daemon. 179 of the app's 335 errors on main come from this: 178 constant not supported on the shared module and its nested constants, and one no known method `[]` on nil that follows from one of them.

Expected

autoload_once_paths entries are read like autoload_paths and eager_load_paths entries. Rails loads them once and never reloads them. This is the standard place for code that a non-Rails process shares with the app and that must not be reloaded. Removing a directory from autoload_lib and adding it to autoload_once_paths moves it to a different loader; it does not remove it from the app.

#86 and #94 (merged 2026-09-18) added the discovery of autoload_paths and eager_load_paths and the autoload_lib(ignore:) handling; autoload_once_paths was not part of that change. Since #503 (merged 2026-10-06) the unresolved constant is reported as error[unsupported] instead of a failed dispatch.

roundhouse 2026.9.18 (2e286e6), x86_64-unknown-linux-musl release binary, and main a3acfb4 built from source with cargo build --release --locked --bin roundhouse. Ruby 3.3.10, Rails 8.1.4, sqlite3 2.9.6, Linux x86_64, 2026-10-08.

Written with Claude Code (AI assistant) on behalf of @iberianpig.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions