Module: Kino::Check

Defined in:
lib/kino/check.rb

Overview

The shareability doctor behind kino --check: explains WHY an app can't run in :ractor mode, instead of leaving you to decode Ractor::IsolationError one ivar at a time.

The walk is strictly non-mutating: Ractor.make_shareable would freeze the user's object graph, so we never call it. Instead we recurse into whatever Ractor.shareable? rejects and name the leaves: instance variables by path, proc captures by variable name and definition site, and the class-ivar trap that bites class-style apps (a Class is always "shareable", but reading its unshareable ivars from a worker ractor raises on the first request).

Defined Under Namespace

Classes: Finding

Constant Summary collapse

MAX_FINDINGS =

Stop after this many findings: the first few name the problem.

20
MAX_NODES =

Walk budget, so a pathological object graph cannot hang the check.

5_000

Class Method Summary collapse

Class Method Details

Pretty-printed report; returns true when the app is ractor-ready.

Parameters:

  • app (#call)

    a Rack application

  • io (IO) (defaults to: $stdout)

    where to print

Returns:

  • (Boolean)


57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/kino/check.rb', line 57

def print_report(app, io: $stdout)
  result = report(app)
  if result[:shareable]
    io.puts CLI.paint("32", "check: app is Ractor-shareable — mode :ractor will work", io: io)
    true
  else
    io.puts CLI.red("check: app is NOT Ractor-shareable", io: io)
    result[:findings].each { |finding| io.puts "  - #{finding}" }
    io.puts dim_hint(io)
    false
  end
end

.report(app) ⇒ Hash

Returns +Boolean, findings: Array+.

Parameters:

  • app (#call)

    a Rack application (or a Class/Module used as one)

Returns:

  • (Hash)

    +Boolean, findings: Array+



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/kino/check.rb', line 34

def report(app)
  findings = []
  seen = {}.compare_by_identity
  budget = {nodes: 0}

  if app.is_a?(Module)
    # Classes/modules pass Ractor.shareable? unconditionally, but their
    # unshareable class-level state is main-ractor-only at runtime.
    scan_module(app, "app (#{app.inspect})", findings)
    {shareable: findings.empty?, findings: findings}
  elsif Ractor.shareable?(app)
    {shareable: true, findings: []}
  else
    walk(app, "app", findings, seen, budget)
    findings << Finding.new(path: "app", message: unshareable_note(app)) if findings.empty?
    {shareable: false, findings: findings}
  end
end