ractor-rails-shim 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +101 -0
- data/LICENSE.txt +21 -0
- data/README.md +265 -0
- data/exe/ractor-rails-check +39 -0
- data/lib/ractor_rails_shim/check.rb +190 -0
- data/lib/ractor_rails_shim/fallback_ies.rb +33 -0
- data/lib/ractor_rails_shim/patches/action_controller.rb +301 -0
- data/lib/ractor_rails_shim/patches/action_dispatch.rb +851 -0
- data/lib/ractor_rails_shim/patches/action_view.rb +582 -0
- data/lib/ractor_rails_shim/patches/active_model_attribute.rb +97 -0
- data/lib/ractor_rails_shim/patches/active_record_model_schema.rb +58 -0
- data/lib/ractor_rails_shim/patches/active_support.rb +1085 -0
- data/lib/ractor_rails_shim/patches/activerecord.rb +1727 -0
- data/lib/ractor_rails_shim/patches/class_attribute.rb +194 -0
- data/lib/ractor_rails_shim/patches/core.rb +806 -0
- data/lib/ractor_rails_shim/patches/devise.rb +172 -0
- data/lib/ractor_rails_shim/patches/execution_wrapper.rb +76 -0
- data/lib/ractor_rails_shim/patches/kaminari.rb +82 -0
- data/lib/ractor_rails_shim/patches/make_shareable.rb +853 -0
- data/lib/ractor_rails_shim/patches/mattr_accessor.rb +162 -0
- data/lib/ractor_rails_shim/patches/openssl.rb +58 -0
- data/lib/ractor_rails_shim/patches/orm_adapter.rb +32 -0
- data/lib/ractor_rails_shim/patches/polymorphic_routes.rb +115 -0
- data/lib/ractor_rails_shim/patches/propshaft.rb +110 -0
- data/lib/ractor_rails_shim/patches/rack.rb +160 -0
- data/lib/ractor_rails_shim/patches/rails_module.rb +192 -0
- data/lib/ractor_rails_shim/patches/route_helpers.rb +119 -0
- data/lib/ractor_rails_shim/patches/rubygems.rb +65 -0
- data/lib/ractor_rails_shim/patches/url_helpers.rb +78 -0
- data/lib/ractor_rails_shim/patches/warden.rb +195 -0
- data/lib/ractor_rails_shim/patches/zeitwerk_registry.rb +124 -0
- data/lib/ractor_rails_shim/patches.rb +67 -0
- data/lib/ractor_rails_shim/version.rb +5 -0
- data/lib/ractor_rails_shim/version_check.rb +88 -0
- data/lib/ractor_rails_shim.rb +33 -0
- metadata +104 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patch for Devise (5.0). Two blockers for worker Ractors:
|
|
4
|
+
#
|
|
5
|
+
# 1. Devise::Controllers::UrlHelpers#generate_helpers! defines every helper
|
|
6
|
+
# (session_path, new_session_path, user_password_path, ...) via
|
|
7
|
+
# `define_method method do |resource_or_scope, *args| ... end` — a block
|
|
8
|
+
# that captures the MAIN ractor's binding. Calling `session_path(...)` from
|
|
9
|
+
# a worker raises "defined with an un-shareable Proc in a different Ractor".
|
|
10
|
+
# Fix: redefine each helper via string eval (no captured binding); the body
|
|
11
|
+
# replicates the original exactly, delegating to the underlying Rails route
|
|
12
|
+
# helper via `context.send(method, *args)` (the real helpers are already
|
|
13
|
+
# patched in action_dispatch.rb).
|
|
14
|
+
#
|
|
15
|
+
# 2. The helper body (and Devise::Mapping.find_scope!) reads `Devise.mappings`,
|
|
16
|
+
# which reads the `@@mappings` class variable — unreadable from a worker
|
|
17
|
+
# Ractor. Fix: snapshot `Devise.mappings` as a shareable Hash at prepare
|
|
18
|
+
# time and have the patched `Devise.mappings` reader return it in workers
|
|
19
|
+
# (skipping the main-ractor route reload).
|
|
20
|
+
|
|
21
|
+
module RactorRailsShim
|
|
22
|
+
# Devise defines several top-level mutable Array/Hash constants
|
|
23
|
+
# (TRUE_VALUES, FALSE_VALUES, NO_INPUT, ALL, CONTROLLERS, ROUTES,
|
|
24
|
+
# STRATEGIES, URL_HELPERS) that are NOT frozen, hence non-shareable — a
|
|
25
|
+
# worker Ractor raises "can not access non-shareable objects in constant
|
|
26
|
+
# Devise::TRUE_VALUES ..." when it reads them (e.g. remember_me? ->
|
|
27
|
+
# TRUE_VALUES). These are populated during boot and never mutated in
|
|
28
|
+
# :ractor mode (frozen graph, no reloading), so deep-freeze + const_set
|
|
29
|
+
# them shareable at prepare time.
|
|
30
|
+
SHAREABLE_CONSTANTS.concat([
|
|
31
|
+
"Devise::TRUE_VALUES",
|
|
32
|
+
"Devise::FALSE_VALUES",
|
|
33
|
+
"Devise::NO_INPUT",
|
|
34
|
+
"Devise::ALL",
|
|
35
|
+
"Devise::CONTROLLERS",
|
|
36
|
+
"Devise::ROUTES",
|
|
37
|
+
"Devise::STRATEGIES",
|
|
38
|
+
"Devise::URL_HELPERS",
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
# Devise::FailureApp.call memoizes its Rack endpoint in a class-level ivar
|
|
43
|
+
# (`@respond ||= action(:respond)`). A worker Ractor cannot set instance
|
|
44
|
+
# variables on a shared class/module, so calling it raises "can not set
|
|
45
|
+
# instance variables of classes/modules by non-main Ractors". Route the
|
|
46
|
+
# memoized endpoint through IsolatedExecutionState (per-Ractor), so each
|
|
47
|
+
# worker builds and caches its own copy without mutating the shared class.
|
|
48
|
+
def _install_devise_failure_app_patch
|
|
49
|
+
return if @devise_failure_app_patched
|
|
50
|
+
@devise_failure_app_patched = true
|
|
51
|
+
_register_patch :devise_failure_app, "5.0"
|
|
52
|
+
return unless defined?(::Devise::FailureApp)
|
|
53
|
+
::Devise::FailureApp.singleton_class.module_eval <<-'RUBY', __FILE__, __LINE__ + 1
|
|
54
|
+
def call(env)
|
|
55
|
+
respond = ActiveSupport::IsolatedExecutionState[:ractor_rails_shim_devise_failure_respond] ||= action(:respond)
|
|
56
|
+
respond.call(env)
|
|
57
|
+
end
|
|
58
|
+
RUBY
|
|
59
|
+
# Devise::FailureApp#relative_url_root reads `config.action_controller
|
|
60
|
+
# .try(:relative_url_root)`. `config.action_controller` is a
|
|
61
|
+
# Rails::Railtie::Configuration whose `relative_url_root` is NOT a real
|
|
62
|
+
# method, so `try` triggers `method_missing`, which reads the `@@options`
|
|
63
|
+
# class variable — unreadable from a worker Ractor. `Rails.application
|
|
64
|
+
# .config.relative_url_root` IS a real method on
|
|
65
|
+
# Rails::Application::Configuration (no method_missing, no class var), so
|
|
66
|
+
# use just that; for the common case (no relative root) it returns nil.
|
|
67
|
+
if defined?(::Devise::FailureApp)
|
|
68
|
+
::Devise::FailureApp.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
69
|
+
def relative_url_root
|
|
70
|
+
@relative_url_root ||= Rails.application.config.relative_url_root
|
|
71
|
+
end
|
|
72
|
+
RUBY
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Devise::Models::Authenticatable::ClassMethods#devise_parameter_filter
|
|
77
|
+
# memoizes `@devise_parameter_filter ||= Devise::ParameterFilter.new(...)`
|
|
78
|
+
# on the MODEL CLASS. A worker Ractor cannot set an instance variable on a
|
|
79
|
+
# shared class/module, so calling it raises "can not set instance variables
|
|
80
|
+
# of classes/modules by non-main Ractors". Route the memoized filter through
|
|
81
|
+
# IsolatedExecutionState (per-Ractor), keyed by the (shared, stable) class
|
|
82
|
+
# object_id. `case_insensitive_keys` / `strip_whitespace_keys` are
|
|
83
|
+
# class_attribute values the shim already routes through IES, so they read
|
|
84
|
+
# fine in a worker.
|
|
85
|
+
def _install_devise_authenticatable_patch
|
|
86
|
+
return if @devise_authenticatable_patched
|
|
87
|
+
@devise_authenticatable_patched = true
|
|
88
|
+
_register_patch :devise_authenticatable, "5.0"
|
|
89
|
+
return unless defined?(::Devise::Models::Authenticatable::ClassMethods)
|
|
90
|
+
::Devise::Models::Authenticatable::ClassMethods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
91
|
+
def devise_parameter_filter
|
|
92
|
+
key = :"ractor_rails_shim_devise_param_filter_\#{object_id}"
|
|
93
|
+
v = ActiveSupport::IsolatedExecutionState[key]
|
|
94
|
+
return v unless v.nil?
|
|
95
|
+
f = Devise::ParameterFilter.new(case_insensitive_keys, strip_whitespace_keys)
|
|
96
|
+
ActiveSupport::IsolatedExecutionState[key] = f
|
|
97
|
+
f
|
|
98
|
+
end
|
|
99
|
+
RUBY
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def _install_devise_url_helpers_patch
|
|
103
|
+
return if @devise_url_helpers_patched
|
|
104
|
+
@devise_url_helpers_patched = true
|
|
105
|
+
_register_patch :devise_url_helpers, "5.0"
|
|
106
|
+
return unless defined?(::Devise::Controllers::UrlHelpers)
|
|
107
|
+
|
|
108
|
+
mod = ::Devise::Controllers::UrlHelpers
|
|
109
|
+
|
|
110
|
+
# Patch Devise.mappings FIRST (before the snapshot below) so every
|
|
111
|
+
# subsequent read — including the snapshot — does NOT trigger a route
|
|
112
|
+
# reload. Routes are fully drawn by Rails.application.initialize! before
|
|
113
|
+
# prepare_for_ractors! runs, so @@mappings is already populated in main;
|
|
114
|
+
# workers read the shareable snapshot instead. The ORIGINAL
|
|
115
|
+
# Devise.mappings calls reload_routes_unless_loaded, which during
|
|
116
|
+
# prepare collapses the RouteSet to a single railtie route (rails/info)
|
|
117
|
+
# that then gets frozen into the shared app graph — breaking routing for
|
|
118
|
+
# every worker Ractor.
|
|
119
|
+
if defined?(::Devise) && ::Devise.respond_to?(:mappings)
|
|
120
|
+
::Devise.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
121
|
+
def mappings
|
|
122
|
+
return RactorRailsShim::DEVISE_MAPPINGS if !Ractor.main? && RactorRailsShim.const_defined?(:DEVISE_MAPPINGS)
|
|
123
|
+
::Devise.class_variable_get(:@@mappings)
|
|
124
|
+
end
|
|
125
|
+
RUBY
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Capture a shareable snapshot of Devise.mappings in MAIN (after routes
|
|
129
|
+
# are drawn). The real Mapping objects hold an unshareable failure-app
|
|
130
|
+
# lambda and a default-proc Hash, so we can't share them directly; build
|
|
131
|
+
# a DeviseMappingSnapshot per scope (see make_shareable.rb). Workers read
|
|
132
|
+
# this via the patched Devise.mappings reader.
|
|
133
|
+
if Ractor.main? && defined?(::Devise)
|
|
134
|
+
begin
|
|
135
|
+
h = ::Devise.mappings
|
|
136
|
+
snap = {}
|
|
137
|
+
h.each { |scope, mapping| snap[scope] = _devise_mapping_snapshot(mapping) }
|
|
138
|
+
snap = Ractor.make_shareable(snap) rescue nil
|
|
139
|
+
const_set(:DEVISE_MAPPINGS, snap) if snap
|
|
140
|
+
rescue
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Redefine each generated helper via string eval (no captured binding).
|
|
146
|
+
# Replicate the EXACT body of Devise::Controllers::UrlHelpers.generate_helpers!
|
|
147
|
+
# (lib/devise/controllers/url_helpers.rb): the helper does NOT call the
|
|
148
|
+
# alias method on the context — it reconstructs the REAL route helper name
|
|
149
|
+
# from `action` + `scope` + `module_name` (e.g. alias `session_path` ->
|
|
150
|
+
# real `user_session_path`) and sends THAT to the context. Bake in
|
|
151
|
+
# `action`/`module_name`/`path_or_url` as literals; `scope` is resolved
|
|
152
|
+
# per-call from the argument, so interpolate it at runtime (\\#{scope}).
|
|
153
|
+
routes = ::Devise::URL_HELPERS.slice(*(::Devise.mappings.values.map(&:used_helpers).flatten.uniq))
|
|
154
|
+
routes.each do |module_name, actions|
|
|
155
|
+
[:path, :url].each do |path_or_url|
|
|
156
|
+
actions.each do |action|
|
|
157
|
+
action_prefix = action ? "#{action}_" : ""
|
|
158
|
+
method = :"#{action_prefix}#{module_name}_#{path_or_url}"
|
|
159
|
+
mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
160
|
+
def #{method}(resource_or_scope, *args)
|
|
161
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
|
162
|
+
router_name = Devise.mappings[scope].router_name
|
|
163
|
+
context = router_name ? send(router_name) : _devise_route_context
|
|
164
|
+
context.send("#{action_prefix}\#{scope}_#{module_name}_#{path_or_url}", *args)
|
|
165
|
+
end
|
|
166
|
+
RUBY
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patch ActiveSupport::ExecutionWrapper.active_key to not write a class
|
|
4
|
+
# ivar from a worker Ractor. The original is `@active_key ||= :"..."`,
|
|
5
|
+
# a raw class-ivar write — illegal from non-main Ractors. The value is a
|
|
6
|
+
# frozen Symbol (pure function of object_id), so we route the cache
|
|
7
|
+
# through IES (per-Ractor; each Ractor computes the same Symbol from the
|
|
8
|
+
# same object_id, so the cached value is identical across Ractors).
|
|
9
|
+
# ExecutionWrapper is the base for Reloader/Executor; `active_key` is
|
|
10
|
+
# called on every request via ActionDispatch::Executor middleware.
|
|
11
|
+
#
|
|
12
|
+
# Also patches ActiveSupport::Callbacks#run_callbacks to tolerate nil
|
|
13
|
+
# __callbacks, and ActiveSupport::Notifications.notifier to not read @notifier.
|
|
14
|
+
|
|
15
|
+
module RactorRailsShim
|
|
16
|
+
class << self
|
|
17
|
+
def install_execution_wrapper
|
|
18
|
+
return if @exec_wrapper_patched
|
|
19
|
+
@exec_wrapper_patched = true
|
|
20
|
+
_register_patch :execution_wrapper, "8.1"
|
|
21
|
+
if defined?(::ActiveSupport::ExecutionWrapper)
|
|
22
|
+
patch_execution_wrapper!
|
|
23
|
+
else
|
|
24
|
+
@ew_tp = TracePoint.new(:class) do |trace|
|
|
25
|
+
if defined?(::ActiveSupport::ExecutionWrapper) && !@exec_wrapper_registry_patched
|
|
26
|
+
@ew_tp.disable
|
|
27
|
+
patch_execution_wrapper!
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
@ew_tp.enable
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def patch_execution_wrapper!
|
|
35
|
+
return if @exec_wrapper_registry_patched
|
|
36
|
+
@exec_wrapper_registry_patched = true
|
|
37
|
+
ew = ::ActiveSupport::ExecutionWrapper
|
|
38
|
+
key = :ractor_rails_shim_exec_wrapper_active_key
|
|
39
|
+
key_str = key.inspect
|
|
40
|
+
# active_key returns :"active_execution_wrapper_<object_id>"; a frozen
|
|
41
|
+
# Symbol is shareable. Compute it once per Ractor and cache in IES.
|
|
42
|
+
ew.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
43
|
+
def active_key
|
|
44
|
+
v = ActiveSupport::IsolatedExecutionState[#{key_str}]
|
|
45
|
+
return v unless v.nil?
|
|
46
|
+
sym = :"active_execution_wrapper_\#{object_id}"
|
|
47
|
+
ActiveSupport::IsolatedExecutionState[#{key_str}] = sym
|
|
48
|
+
sym
|
|
49
|
+
end
|
|
50
|
+
RUBY
|
|
51
|
+
|
|
52
|
+
# Patch ActiveSupport::Callbacks#run_callbacks to tolerate a nil
|
|
53
|
+
# __callbacks (the case in worker Ractors whose class_attribute fallback
|
|
54
|
+
# couldn't be made shareable because callback chains hold frozen,
|
|
55
|
+
# self-capturing Procs). For a frozen, read-only shared app the boot-time
|
|
56
|
+
# callbacks (ExecutionContext push/pop, CurrentAttributes clear) already
|
|
57
|
+
# ran in the main Ractor at boot; worker Ractors don't need to re-run
|
|
58
|
+
# them per request (CurrentAttributes/ExecutionContext are thread-local,
|
|
59
|
+
# hence per-Ractor, and start empty in a fresh worker). When __callbacks
|
|
60
|
+
# is nil, run_callbacks just yields the block — matching the empty-chain
|
|
61
|
+
# fast path in the original. (Method body lives in active_support.rb.)
|
|
62
|
+
_install_callbacks_nil_safe_patch
|
|
63
|
+
|
|
64
|
+
# Patch ActiveSupport::Notifications.notifier to not read the @notifier
|
|
65
|
+
# class ivar from a worker Ractor. The original is `attr_accessor
|
|
66
|
+
# :notifier` with `@notifier = Fanout.new` set at module load — a raw
|
|
67
|
+
# class ivar holding a Fanout (which has a Mutex + subscriber Procs,
|
|
68
|
+
# both unshareable). Workers get their own per-Ractor Fanout (no
|
|
69
|
+
# subscribers — instrumentation is a no-op in workers, which is correct
|
|
70
|
+
# for a read-only shared app where log subscribers already ran in main).
|
|
71
|
+
# `notifier` is read by `instrumenter` (per-request via Rails::Rack::Logger).
|
|
72
|
+
# (Method body lives in active_support.rb.)
|
|
73
|
+
_install_notifications_notifier_patch
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Patch Kaminari::config to not read the @_config class ivar from a worker
|
|
4
|
+
# Ractor.
|
|
5
|
+
#
|
|
6
|
+
# Blocker 2 (from NEXT_STEPS.md):
|
|
7
|
+
# Kaminari.config reads @_config directly at kaminari/config.rb:14:
|
|
8
|
+
# def self.config; @_config ||= Kaminari::Configuration.new end
|
|
9
|
+
# Not mattr_accessor-backed, so the shim's mattr rewrite doesn't catch it.
|
|
10
|
+
# Reading @_config from a worker raises
|
|
11
|
+
# Ractor::IsolationError (@_config from Kaminari).
|
|
12
|
+
#
|
|
13
|
+
# Fix: Targeted patch like the Warden hooks patch. Route Kaminari.config
|
|
14
|
+
# through IES; in the main ractor return the existing @_config; in workers
|
|
15
|
+
# return a shareable fallback (the config object made shareable via
|
|
16
|
+
# Ractor.make_shareable). The config is a simple value object (integers,
|
|
17
|
+
# symbols, nils) — all shareable when frozen.
|
|
18
|
+
|
|
19
|
+
module RactorRailsShim
|
|
20
|
+
class << self
|
|
21
|
+
def _install_kaminari_config_patch
|
|
22
|
+
return if @kaminari_patched
|
|
23
|
+
@kaminari_patched = true
|
|
24
|
+
_register_patch :kaminari_config, "8.1"
|
|
25
|
+
return unless defined?(::Kaminari)
|
|
26
|
+
|
|
27
|
+
# In the main ractor, capture the config object and make it shareable.
|
|
28
|
+
shareable_config = nil
|
|
29
|
+
if Ractor.main?
|
|
30
|
+
begin
|
|
31
|
+
cfg = ::Kaminari.instance_variable_get(:@_config) rescue nil
|
|
32
|
+
if cfg
|
|
33
|
+
begin
|
|
34
|
+
Ractor.make_shareable(cfg)
|
|
35
|
+
shareable_config = cfg
|
|
36
|
+
rescue => e
|
|
37
|
+
# If the config can't be made shareable (unlikely — it's all
|
|
38
|
+
# integers/symbols/nil), build a fresh one with defaults.
|
|
39
|
+
shareable_config = Ractor.make_shareable(::Kaminari::Config.new)
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
shareable_config = Ractor.make_shareable(::Kaminari::Config.new)
|
|
43
|
+
end
|
|
44
|
+
rescue => e
|
|
45
|
+
# Best-effort
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
shareable_config ||= Ractor.make_shareable(::Kaminari::Config.new) rescue nil
|
|
49
|
+
|
|
50
|
+
# Store the shareable config as a constant so workers can read it.
|
|
51
|
+
if shareable_config
|
|
52
|
+
verbose, $VERBOSE = $VERBOSE, nil
|
|
53
|
+
begin
|
|
54
|
+
const_set(:KAMINARI_SHAREABLE_CONFIG, shareable_config)
|
|
55
|
+
ensure
|
|
56
|
+
$VERBOSE = verbose
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
k_key = :ractor_rails_shim_kaminari_config
|
|
61
|
+
k_key_str = k_key.inspect
|
|
62
|
+
::Kaminari.singleton_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
63
|
+
def config
|
|
64
|
+
v = ActiveSupport::IsolatedExecutionState[#{k_key_str}]
|
|
65
|
+
return v unless v.nil?
|
|
66
|
+
if Ractor.main? && instance_variable_defined?(:@_config)
|
|
67
|
+
@_config
|
|
68
|
+
else
|
|
69
|
+
RactorRailsShim::KAMINARI_SHAREABLE_CONFIG
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def config=(val)
|
|
74
|
+
ActiveSupport::IsolatedExecutionState[#{k_key_str}] = val
|
|
75
|
+
end
|
|
76
|
+
RUBY
|
|
77
|
+
|
|
78
|
+
# Register so the shareable fallback builder knows about it.
|
|
79
|
+
CLASS_ATTRIBUTES << ["Kaminari", :config, k_key, nil] if shareable_config
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|