Module: RactorRailsShim::LoggerIONeutralizer
- Extended by:
- RoleDefaults
- Defined in:
- lib/ractor_rails_shim/roles/logger_io_neutralizer.rb
Class Method Summary collapse
-
.call(app) ⇒ Object
Neutralize the logger IO reachable from
appso the subsequentRactor.make_shareable(app)doesn't freeze the process's real$stdout/$stderr. -
.configure(funnel: nil, noop_log_dev_class: nil) ⇒ Object
Inject the collaborators.
-
.funnel ⇒ Object
The active funnel: the injected one if configured, else the facade lookup (
RactorRailsShim::Funnel.method(:swallow)). -
.noop_log_dev_class ⇒ Object
The active NoOpLogDev class: the injected one if configured, else the facade lookup (
RactorRailsShim.singleton_class::NoOpLogDev). -
.reset_configuration ⇒ Object
Restore the default (facade-lookup) collaborators.
Methods included from RoleDefaults
default_funnel, default_reassign_shareable_const, default_safe_const_get
Class Method Details
.call(app) ⇒ Object
Neutralize the logger IO reachable from app so the subsequent
Ractor.make_shareable(app) doesn't freeze the process's real
$stdout/$stderr. Best-effort: failures on individual ivar
swaps (e.g. frozen owners) are funneled through funnel so
debug= surfaces them. Returns nil (the mutate is in-place).
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 109 110 111 112 113 114 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 |
# File 'lib/ractor_rails_shim/roles/logger_io_neutralizer.rb', line 72 def self.call(app) # A frozen, shareable no-op BroadcastLogger (no broadcasts → no # IO) to swap in for the app-instance logger graph. noop_logger = ::ActiveSupport::BroadcastLogger.new noop_logger.freeze Ractor.make_shareable(noop_logger) # Replace the app-instance logger + any IO reachable from the app # graph. Walk ivars + Array/Hash children, tracking seen objects # by object_id to avoid cycles. seen = {} stack = [app] until stack.empty? o = stack.pop next if o.equal?(nil) || seen[o.object_id] seen[o.object_id] = true # Reuse ShareabilityTraversal.each_ivar for the ivar walk (Issue # #39, POODR §4b) — the rescue scaffolding has one home there. # The per-ivar branching (@logger -> BroadcastLogger, IO -> # NoOpLogDev, else -> recurse) stays inline here; it's role- # specific and each_ivar yields [iv, v] pairs cleanly. ShareabilityTraversal.each_ivar(o) do |iv, v| if iv == :@logger # Replace the app-instance / config logger with the no-op # (so the frozen app graph holds no live IO). Best-effort; # funnel through `funnel` so a frozen-owner failure is # traceable under debug=. funnel.call("neutralize logger ivar") do o.instance_variable_set(iv, noop_logger) end elsif v.is_a?(::IO) && (v == $stdout || v == $stderr || v == STDOUT || v == STDERR) # Any stray IO reference → a shareable no-op sink, built # by the injected `noop_log_dev_class` collaborator. sink = noop_log_dev_class.new sink.freeze Ractor.make_shareable(sink) funnel.call("neutralize logger IO ivar") do o.instance_variable_set(iv, sink) end elsif v stack << v end end # Use the shared CONTAINER_WALKERS dispatch table to push children # onto the stack (Issue #32 — reuse traversal from # ShareabilityTraversal instead of duplicating an is_a? chain). walker = ShareabilityTraversal::CONTAINER_WALKERS.find { |klass, _| o.is_a?(klass) }&.last if walker walker.call(o) { |c, _| stack << c if c } elsif ShareabilityTraversal.enumerable_but_not_basic?(o) o.each { |e| stack << e if e } rescue nil end end # Re-point the MAIN ractor's Rails.logger at a fresh live logger # (not reachable from the frozen app) so main keeps logging after # the app is made shareable. `$stderr` is each-Ractor-local and not # in the app graph, so it stays mutable. Use the same shape Rails # uses (BroadcastLogger broadcasting to a Logger writing $stderr). # Guard: the patched `Rails.logger=` (from install_rails_module) # calls `super`; a fake `::Rails` module in unit specs has no # superclass `logger=`, so rescue NoMethodError there. Against real # Rails the `super` succeeds and the rescue is never hit. if Ractor.main? && defined?(::Rails) begin live = ::ActiveSupport::BroadcastLogger.new(::Logger.new($stderr)) ::Rails.logger = live rescue NoMethodError # Fake Rails in a unit spec — no superclass logger= to super. end end nil end |
.configure(funnel: nil, noop_log_dev_class: nil) ⇒ Object
Inject the collaborators. funnel is a callable responding to
call(label) { block } that runs the block and rescues
StandardError (matching _swallow). noop_log_dev_class is a
class responding to .new returning a sink object the role will
freeze + make shareable. Passing nil (or calling
reset_configuration) restores the facade-lookup defaults.
44 45 46 47 |
# File 'lib/ractor_rails_shim/roles/logger_io_neutralizer.rb', line 44 def self.configure(funnel: nil, noop_log_dev_class: nil) @funnel = funnel @noop_log_dev_class = noop_log_dev_class end |
.funnel ⇒ Object
The active funnel: the injected one if configured, else the
facade lookup (RactorRailsShim::Funnel.method(:swallow)).
57 58 59 |
# File 'lib/ractor_rails_shim/roles/logger_io_neutralizer.rb', line 57 def self.funnel @funnel || default_funnel end |
.noop_log_dev_class ⇒ Object
The active NoOpLogDev class: the injected one if configured, else
the facade lookup (RactorRailsShim.singleton_class::NoOpLogDev).
63 64 65 |
# File 'lib/ractor_rails_shim/roles/logger_io_neutralizer.rb', line 63 def self.noop_log_dev_class @noop_log_dev_class || RactorRailsShim.singleton_class.const_get(:NoOpLogDev) end |
.reset_configuration ⇒ Object
Restore the default (facade-lookup) collaborators. Test seam.
50 51 52 53 |
# File 'lib/ractor_rails_shim/roles/logger_io_neutralizer.rb', line 50 def self.reset_configuration @funnel = nil @noop_log_dev_class = nil end |