skip to content

A Ruby static-site generator's plugin loader runs each plugin twice after a rebuild; how do require, load and wrap explain and fix it?

level: seniorimportance: should knowfreq 24%

answer

  1. load re-executes, require remembers
  2. side effects repeat with the file
  3. already initialized constant
  4. load(path, true) anonymous module
  5. a module as wrap since 3.1

basics

~20 s

The loader uses load, which re-runs every plugin file on each rebuild, so registration side effects repeat. Either require plugins once, or keep load and reset the registry first, isolating each build's constants with load(path, true) or a fresh module.

solid answer

~40 s

`load` executes a file every time it is called and never consults `$LOADED_FEATURES`, so a loader that calls `load` for each plugin on every rebuild re-runs the plugin's top level: `Sitegen.register(...)` adds a second hook, constants trigger `already initialized constant` warnings, and methods are redefined. If plugins never change while the process runs, use `require` (or `require_relative`), which runs each file once. If edits must be picked up, keep `load` but make it idempotent: clear the registry before reloading and run each file with a wrap argument, `load(path, true)` for an anonymous module or, since Ruby 3.1, `load(path, a_module)` for a named one, so each build gets a fresh namespace instead of redefining constants in `Object`.

code

ruby · 6 lines
ruby
# plugins never change at run time: run each file once
Dir.glob(File.join(PLUGIN_DIR, "*.rb")).sort.each { |path| require path }

# second call during a rebuild: every require returns false
Dir.glob(File.join(PLUGIN_DIR, "*.rb")).sort.map { |path| require path }
# => [false, false, false]

go deeper

for a junior

Recall that load runs a file every time and require only once, and connect that to a plugin running twice.

for a middle

Explain why reloading repeats registration and triggers already initialized constant warnings, and what load's wrap argument changes.

for a senior

Design the loader: require when plugins are static; otherwise idempotent registration, a per-build module via load(path, mod), and absolute paths to avoid load-path shadowing.

for a principal

Weigh a reloadable plugin API against its cost: a plugin contract that forbids top-level side effects is simpler than any reloading machinery.

## The scenario A static-site generator lets users drop Ruby files into `plugins/`. Each plugin registers a hook when its file runs: ```ruby # plugins/reading_time.rb class ReadingTime WORDS_PER_MINUTE = 200 def call(page) page.meta[:minutes] = page.words / WORDS_PER_MINUTE end end Sitegen.register(:page, ReadingTime.new) ``` In watch mode, the generator rebuilds after every change and calls its loader again. After the first rebuild every page gets processed twice; after the second, three times; and the console shows `already initialized constant ReadingTime::WORDS_PER_MINUTE`. ## Why it happens | Loader call | First build | Rebuild | |---|---|---| | `load path` | runs file, registers hook | runs file again, registers a second hook | | `require path` | runs file, registers hook | returns `false`, nothing runs | | `load path, true` | runs file inside a fresh anonymous module | runs again inside another fresh module | - **`load` has no memory.** It is designed to re-execute, so every side effect at a file's top level, such as registration, monkey patches or `at_exit` hooks, happens once per call. - **Constants are reopened, not replaced cleanly.** Reopening `class ReadingTime` adds to the existing class; assigning `WORDS_PER_MINUTE` again prints `already initialized constant`, and methods are redefined in place. - **`require` would not double-run**, but it would also ignore edits, which is why the author reached for `load`. ## Fix 1: plugins that do not change at run time Use `require` (with absolute paths from `Dir.glob(File.join(dir, "*.rb"))`) or `require_relative`. Each file runs exactly once per process, and the loader can be called on every rebuild safely because further calls return `false`. ## Fix 2: plugins that must reload 1. **Make registration idempotent.** Clear the registry at the start of each load pass (`Sitegen.hooks.clear`), or key hooks by plugin name so a second registration replaces the first. 2. **Isolate definitions with `wrap`.** `load(path, true)` runs the file under a new anonymous module: constants and methods the file defines at its top level belong to that module rather than `Object`, so a rebuild creates new ones instead of redefining the old ones, and the warnings disappear. 3. **Name the namespace when you need to reach it.** Since Ruby 3.1 the second argument may be a module: `load(path, mod)` with `mod = Module.new` created per build lets the loader inspect `mod.constants` afterwards and drop the whole module on the next rebuild. ```ruby def load_plugins(dir) Sitegen.hooks.clear Dir.glob(File.join(dir, "*.rb")).sort.map do |path| mod = Module.new load(path, mod) # top-level constants go into mod mod end end ``` ## Limits of `wrap` - It isolates **definitions**, not **effects**: a plugin that calls `Sitegen.register` or reopens a core class (`class ::String`) still changes shared state each time. - Code in the wrapped file can still reach any constant through an explicit `::` path. - `require` calls inside the plugin behave normally, with the global once-only rule. ## What interviewers want to hear - Name the mechanism: `load` re-executes; `require` records the absolute path in `$LOADED_FEATURES` and skips repeats. - Separate the two problems: repeated side effects (fix with idempotent registration) and redefined constants (fix with `wrap` or `require`). - Choose based on whether plugins can change while the process runs. ## Detecting double loads 1. Watch the warnings: redefining a constant always prints `already initialized constant`, and with `-w` Ruby also prints `method redefined; discarding old ...` for methods defined again. 2. Add a test that calls the loader twice and asserts that the registry size did not change. 3. Log each plugin path as it loads; two log lines for one path in one build is the symptom, and the call site that produced the second is the cause. These checks catch the problem before users see pages processed twice.

  • Does `load(path, true)` stop a plugin from registering its hook a second time?
    No. The wrap argument only changes where the file's top-level constants and methods are defined. A call such as `Sitegen.register(...)` still runs on every `load` and still reaches the shared registry, so registration must be made idempotent separately, for example by clearing the registry before each pass.
  • Why is `require` with a relative name risky for user plugins?
    A bare name such as `require "reading_time"` is searched through `$LOAD_PATH`, so a gem or library file with the same name can win and the plugin is never loaded. Passing the absolute path from `Dir.glob` or using `require_relative` pins the exact file.

saying these in an interview costs you the question

  • load skips a plugin file that has already been loaded once
  • load(path, true) prevents a plugin's side effects from repeating
  • Reopening a class on reload replaces it with a brand-new class
  • require picks up plugin edits made while the process runs
  • The wrap argument of load accepts only true or false in Ruby 4.0