Module: Rigor::Plugin::Isolation
- Defined in:
- lib/rigor/plugin/isolation.rb
Overview
ADR-39 slice 5 — the selectable isolation strategy for target-library invocation. A plugin invokes a
pure method on a trusted target library (e.g. ActiveSupport::Inflector.pluralize("post")) through
Isolation.call; how much the invocation is isolated from Rigor's own process is a configurable strategy
(RIGOR_PLUGIN_ISOLATION env; the exe/rigor launcher maps .rigor.yml's plugins_isolation: onto it
before re-exec). Three backends behind one interface:
none— load into the main space and call directly. Lowest cost; no isolation. Used as the fallback where fork is unavailable; fine because the invoked library is trusted + pure.ruby_box— call inside a Box (Ruby::Box,RUBY_BOX=1). Isolates core-class monkey-patches + lets gem versions coexist, but a native crash in the boxed work still takes the process down (in-process).process(default) — call in a forked worker (Process); returns data over a pipe. The strongest: a child crash (evenSIGSEGV) is contained — the parent survives and declines. Higher cost (fork + IPC).
All three answer with the method's return value, or raise Unavailable (never approximate) when the target library cannot be reached in the chosen strategy — the caller's per-plugin rescue turns that into silence, never a wrong fact.
Defined Under Namespace
Modules: Direct, Process, RubyBox Classes: Unavailable
Constant Summary collapse
- STRATEGIES =
%w[none ruby_box process].freeze
- DEFAULT =
The default strategy.
process(a crash-contained forked worker) is the default: it isolates the target library's monkey-patches + crashes from Rigor with no in-process contamination, and forks a single persistent worker (not one per call). It falls back tononewhere fork is unavailable (see #backend). "process"
Class Method Summary collapse
-
.backend ⇒ Object
The backend module for the configured strategy.
-
.bundle_gem_dirs(bundle_root) ⇒ Object
The RubyGems-shaped directories under a bundler install root: bundler nests them as
<root>/ruby/<ruby-version>/; aBUNDLE_PATHmay also point directly at such a directory. -
.bundle_require_paths(bundle_root) ⇒ Object
Every bundle gem's require paths, newest version per gem name.
-
.call(feature:, receiver:, method:, args:) ⇒ Object
Invokes
receiver.method(*args)on a target library, requiringfeaturefirst, under the configured isolation strategy. -
.require_with_target_bundle(feature, bundle_root) ⇒ Object
Requires
feature, falling back to the analyzed project's bundler install tree. -
.strategy_name ⇒ Object
The configured strategy name (
RIGOR_PLUGIN_ISOLATION), defaulting to DEFAULT for any unset / unrecognised value. -
.target_bundle_root ⇒ Object
The analyzed project's resolved bundler install root (e.g.
<project>/vendor/bundle), or nil. - .target_bundle_root=(root) ⇒ Object
Class Method Details
.backend ⇒ Object
The backend module for the configured strategy. process (including the default) falls back to
Direct where fork is unavailable (Windows / JRuby) so inflection still works rather than silently
degrading — the libraries are trusted + pure, so the main-space fallback is acceptable when no
fork-based isolation can be had.
117 118 119 120 121 122 123 |
# File 'lib/rigor/plugin/isolation.rb', line 117 def backend case strategy_name when "ruby_box" then RubyBox when "none" then Direct else Process.available? ? Process : Direct end end |
.bundle_gem_dirs(bundle_root) ⇒ Object
The RubyGems-shaped directories under a bundler install root: bundler nests them as
<root>/ruby/<ruby-version>/; a BUNDLE_PATH may also point directly at such a directory. Only
directories that actually carry a specifications/ index are returned.
84 85 86 87 88 89 90 |
# File 'lib/rigor/plugin/isolation.rb', line 84 def bundle_gem_dirs(bundle_root) return [] if bundle_root.nil? || bundle_root.to_s.empty? root = bundle_root.to_s (Dir.glob(File.join(root, "ruby", "*")) + [root]) .select { |dir| File.directory?(File.join(dir, "specifications")) } end |
.bundle_require_paths(bundle_root) ⇒ Object
Every bundle gem's require paths, newest version per gem name. Loading a specifications/*.gemspec
evaluates RubyGems-generated metadata of an installed third-party gem — the same trust level as
requiring the gem, which is what the caller is about to do.
72 73 74 75 76 77 78 79 |
# File 'lib/rigor/plugin/isolation.rb', line 72 def bundle_require_paths(bundle_root) bundle_gem_dirs(bundle_root).flat_map do |gem_home| specs = Dir.glob(File.join(gem_home, "specifications", "*.gemspec")) .filter_map { |file| Gem::Specification.load(file) } specs.group_by(&:name) .flat_map { |_, versions| versions.max_by(&:version).full_require_paths } end end |
.call(feature:, receiver:, method:, args:) ⇒ Object
Invokes receiver.method(*args) on a target library, requiring feature first, under the configured
isolation strategy. receiver is a constant name (String), method a Symbol from the caller's
allow-list, and args simple, Marshal-able / inspectable values (Strings) — never free input. Returns
the result, or raises Unavailable.
109 110 111 |
# File 'lib/rigor/plugin/isolation.rb', line 109 def call(feature:, receiver:, method:, args:) backend.call(feature: feature, receiver: receiver, method: method, args: args) end |
.require_with_target_bundle(feature, bundle_root) ⇒ Object
Requires feature, falling back to the analyzed project's bundler install tree. The fallback
appends every bundle gem's full_require_paths (from its RubyGems-generated specifications/
gemspec — the gem's own metadata, so nonstandard require paths like concurrent-ruby's
lib/concurrent-ruby resolve correctly) to $LOAD_PATH and retries. $LOAD_PATH is appended,
never prepended, and only on a failed require: Rigor's own activated gems keep precedence, and a
host environment that carries the gem never consults the bundle. (Gem.paths augmentation was
rejected — Bundler-locked processes silently ignore it.) Under the default process strategy the
mutation happens inside the forked worker only; under none/Direct it lands in the main space,
which is what that strategy means (documented tradeoff — the invoked library is trusted + pure).
59 60 61 62 63 64 65 66 67 |
# File 'lib/rigor/plugin/isolation.rb', line 59 def require_with_target_bundle(feature, bundle_root) require feature rescue ::LoadError added = bundle_require_paths(bundle_root) - $LOAD_PATH raise if added.empty? $LOAD_PATH.concat(added) require feature end |
.strategy_name ⇒ Object
The configured strategy name (RIGOR_PLUGIN_ISOLATION), defaulting to DEFAULT for any unset /
unrecognised value.
100 101 102 103 |
# File 'lib/rigor/plugin/isolation.rb', line 100 def strategy_name name = ENV["RIGOR_PLUGIN_ISOLATION"].to_s STRATEGIES.include?(name) ? name : DEFAULT end |
.target_bundle_root ⇒ Object
The analyzed project's resolved bundler install root (e.g. <project>/vendor/bundle), or nil.
Set from the same bundler.* resolution that feeds bundle sig discovery (O4 / ADR-82 WD9) — by
the runner's pre-passes BEFORE any plugin #prepare runs, by Environment.for_project (which
also covers pool workers), and by rigor plugins' probe. When a target library cannot be
required from Rigor's own gem
environment (the standalone gem install rigortype case — activesupport is deliberately NOT a
runtime dependency), require_with_target_bundle falls back to requiring it from this bundle:
the analyzed Rails project always carries its own activesupport on disk, and loading the
project's locked copy is the higher-fidelity source of inflection rules anyway (ADR-79).
42 43 44 |
# File 'lib/rigor/plugin/isolation.rb', line 42 def target_bundle_root @target_bundle_root end |