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.
-
.safe_class(val) ⇒ Object
Safely derive a value's class name.
-
.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).
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 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/ractor_rails_shim/check.rb', line 125 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 |
.safe_class(val) ⇒ Object
Safely derive a value's class name. BasicObject (and its subclasses)
don't define .class, so calling it raises NoMethodError — fall back
to "BasicObject" in that case.
21 22 23 24 25 |
# File 'lib/ractor_rails_shim/check.rb', line 21 def safe_class(val) val.class.name || val.class.to_s rescue NoMethodError "BasicObject" end |
.scan ⇒ Object
Scan all loaded classes/modules and report class ivars AND class variables holding unshareable values. Returns an array of Finding.
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 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/ractor_rails_shim/check.rb', line 30 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: safe_class(val), 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: safe_class(val), 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.
119 120 121 |
# File 'lib/ractor_rails_shim/check.rb', line 119 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?"
112 113 114 115 116 |
# File 'lib/ractor_rails_shim/check.rb', line 112 def scan_rails scan.select { |f| f.owner.start_with?("Rails", "ActiveRecord", "ActiveSupport", "ActionController", "ActionView", "ActionDispatch", "ActionMailer", "ActiveJob", "ActionCable", "ActionText", "ActionMailbox", "ActiveStorage") } end |