Class: RactorRailsShim::Check

Inherits:
Object
  • Object
show all
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

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

.scanObject

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_appObject

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_railsObject

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