Class: RactorRailsShim::Check
- Inherits:
-
Object
- Object
- RactorRailsShim::Check
- Defined in:
- lib/ractor_rails_shim/check.rb
Overview
Audit an app for Ractor blockers. Inspects loaded classes/modules for class-level instance variables and mattr_accessor usage that would raise Ractor::IsolationError from a non-main Ractor.
Modeled on Kino's kino --check — tell the user exactly what blocks
their app, instead of leaving them to decode IsolationError at runtime.
Defined Under Namespace
Classes: Finding
Class Method Summary collapse
-
.report(print: true) ⇒ Object
Human-readable report.
-
.scan ⇒ Object
Scan all loaded classes/modules and report class ivars AND class variables holding unshareable values.
-
.scan_app ⇒ Object
Scan app + gem classes outside the Rails framework.
-
.scan_rails ⇒ Object
Scan only Rails framework modules (Railties, ActiveRecord, etc.) — the ones the shim targets.
Class Method Details
.report(print: true) ⇒ Object
Human-readable report. Returns a string; also prints to $stderr
if print: is true (default).
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/ractor_rails_shim/check.rb', line 115 def report(print: true) findings = scan rails_findings = findings.select { |f| rails_namespace?(f.owner) } app_findings = findings - rails_findings lines = [] cvar_count = findings.count { |f| f.kind == :cvar } ivar_count = findings.count { |f| f.kind == :ivar } lines << "ractor-rails-shim check: #{findings.size} blocker(s) found" \ " (#{ivar_count} class-ivar, #{cvar_count} class-var)" lines << " (unshareable values in @ivar and @@cvar; reads/writes from a non-main Ractor" lines << " would raise Ractor::IsolationError)" lines << "" %i[rails app].each do |group| grp = group == :rails ? rails_findings : app_findings next if grp.empty? label = group == :rails ? "Rails framework" : "app + gems" lines << "=== #{label} (#{grp.size}) ===" grp.first(50).each do |f| tag = f.kind == :cvar ? " (mattr/cattr — shim targets)" : "" lines << " #{f.owner}#{f.ivar} = #{f.value_class}#{tag}" lines << " #{f.source}" if f.source end if grp.size > 50 lines << " ... and #{grp.size - 50} more (use Check.scan to see all)" end lines << "" end if findings.empty? lines << "no blockers found — app may be Ractor-compatible" else lines << "hints:" lines << " - require \"ractor_rails_shim\" and call RactorRailsShim.install before" lines << " Rails.application is first accessed (early in config/boot.rb)" lines << " - class-var (@@foo) blockers from mattr_accessor/cattr_accessor are" lines << " rerouted by the shim automatically once installed" lines << " - raw class-ivar (@foo) blockers are NOT fixed by the shim; patch the" lines << " gem or use Ractor.make_shareable + a constant for shareable state" lines << " - for per-Ractor mutable state use Ractor.store_if_absent(key) { default }" end out = lines.join("\n") $stderr.puts(out) if print out end |
.scan ⇒ Object
Scan all loaded classes/modules and report class ivars AND class variables holding unshareable values. Returns an array of Finding.
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/ractor_rails_shim/check.rb', line 20 def scan findings = [] seen = {} ObjectSpace.each_object(Module) do |mod| next if mod.name.nil? || mod.name.empty? next if mod.name.start_with?("#", "Ractor::", "Thread::", "Fiber::", "ObjectSpace::") # Inspect class-level instance variables. mod.instance_variables.each do |ivar| next if ivar == :@_sandbox # Rails internal, ignored begin val = mod.instance_variable_get(ivar) rescue => e next end shareable = begin Ractor.shareable?(val) rescue => e false end next if shareable key = "#{mod.name}#{ivar}" next if seen[key] seen[key] = true source = locate(mod, ivar) findings << Finding.new( owner: mod.name, ivar: ivar.to_s, value_class: val.class.name || val.class.to_s, shareable: shareable, source: source, kind: :ivar ) end # Inspect class variables (@@foo) — these back mattr_accessor / # cattr_accessor and are ALSO subject to Ractor::IsolationError # from non-main Ractors (verified on Ruby 4.0.5). begin mod.class_variables.each do |cvar| begin val = mod.class_variable_get(cvar) rescue => e next end shareable = begin Ractor.shareable?(val) rescue => e false end next if shareable key = "#{mod.name}#{cvar}" next if seen[key] seen[key] = true source = locate(mod, cvar) findings << Finding.new( owner: mod.name, ivar: cvar.to_s, value_class: val.class.name || val.class.to_s, shareable: shareable, source: source, kind: :cvar ) end rescue => e # Some modules raise on class_variables enumeration; skip. next end end findings.sort_by { |f| [f.owner, f.ivar] } end |
.scan_app ⇒ Object
Scan app + gem classes outside the Rails framework.
109 110 111 |
# File 'lib/ractor_rails_shim/check.rb', line 109 def scan_app scan - scan_rails end |
.scan_rails ⇒ Object
Scan only Rails framework modules (Railties, ActiveRecord, etc.) — the ones the shim targets. Useful for "is Rails itself clean?"
102 103 104 105 106 |
# File 'lib/ractor_rails_shim/check.rb', line 102 def scan_rails scan.select { |f| f.owner.start_with?("Rails", "ActiveRecord", "ActiveSupport", "ActionController", "ActionView", "ActionDispatch", "ActionMailer", "ActiveJob", "ActionCable", "ActionText", "ActionMailbox", "ActiveStorage") } end |