Skip to content

Commit 1d22bc6

Browse files
committed
Fix thread-safety issues
Based on #2942 (comment) Fixes #2897, Fixes #2942
1 parent 9abc7f9 commit 1d22bc6

File tree

7 files changed

+129
-56
lines changed

7 files changed

+129
-56
lines changed

lib/rails_admin/config/configurable.rb

+5-3
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,16 @@ def with_recurring(option_name, value_proc, default_proc)
3030
# and allows configurations such as
3131
# label { "#{label}".upcase }
3232
# This will use the default definition when called recursively.
33-
if instance_variable_get("@#{option_name}_recurring")
33+
Thread.current[:rails_admin_recurring] ||= {}
34+
Thread.current[:rails_admin_recurring][self] ||= {}
35+
if Thread.current[:rails_admin_recurring][self][option_name]
3436
instance_eval(&default_proc)
3537
else
36-
instance_variable_set("@#{option_name}_recurring", true)
38+
Thread.current[:rails_admin_recurring][self][option_name] = true
3739
instance_eval(&value_proc)
3840
end
3941
ensure
40-
instance_variable_set("@#{option_name}_recurring", false)
42+
Thread.current[:rails_admin_recurring].delete(self)
4143
end
4244

4345
module ClassMethods

lib/rails_admin/config/fields/base.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class Base # rubocop:disable ClassLength
2020

2121
NAMED_INSTANCE_VARIABLES = [
2222
:@parent, :@root, :@section, :@children_fields_registered,
23-
:@associated_model_config, :@group, :@bindings
23+
:@associated_model_config, :@group
2424
].freeze
2525

2626
def initialize(parent, name, properties)

lib/rails_admin/config/proxyable.rb

+13-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,19 @@
22
module RailsAdmin
33
module Config
44
module Proxyable
5-
attr_accessor :bindings
5+
def bindings
6+
Thread.current[:rails_admin_bindings] ||= {}
7+
Thread.current[:rails_admin_bindings][self]
8+
end
9+
10+
def bindings=(new_bindings)
11+
Thread.current[:rails_admin_bindings] ||= {}
12+
if new_bindings.nil?
13+
Thread.current[:rails_admin_bindings].delete(self)
14+
else
15+
Thread.current[:rails_admin_bindings][self] = new_bindings
16+
end
17+
end
618

719
def with(bindings = {})
820
RailsAdmin::Config::Proxyable::Proxy.new(self, bindings)

lib/rails_admin/config/proxyable/proxy.rb

+3-5
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ module RailsAdmin
22
module Config
33
module Proxyable
44
class Proxy < BasicObject
5-
attr_reader :bindings
6-
75
def initialize(object, bindings = {})
86
@object = object
97
@bindings = bindings
@@ -21,12 +19,12 @@ def bind(key, value = nil)
2119

2220
def method_missing(name, *args, &block)
2321
if @object.respond_to?(name)
24-
reset = @object.instance_variable_get('@bindings')
22+
reset = @object.bindings
2523
begin
26-
@object.instance_variable_set('@bindings', @bindings)
24+
@object.bindings = @bindings
2725
response = @object.__send__(name, *args, &block)
2826
ensure
29-
@object.instance_variable_set('@bindings', reset)
27+
@object.bindings = reset
3028
end
3129
response
3230
else
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
require 'spec_helper'
2+
3+
describe RailsAdmin::Config::Configurable do
4+
class ConfigurableTest
5+
include RailsAdmin::Config::Configurable
6+
7+
register_instance_option :foo do
8+
'default'
9+
end
10+
end
11+
12+
subject { ConfigurableTest.new }
13+
14+
describe 'recursion tracking' do
15+
it 'works and use default value' do
16+
subject.instance_eval do
17+
foo { foo }
18+
end
19+
expect(subject.foo).to eq 'default'
20+
end
21+
22+
describe 'with parallel execution' do
23+
before do
24+
subject.instance_eval do
25+
foo do
26+
sleep 0.15
27+
'value'
28+
end
29+
end
30+
end
31+
32+
it 'ensures thread-safety' do
33+
threads = Array.new(2) do |i|
34+
Thread.new do
35+
sleep i * 0.1
36+
expect(subject.foo).to eq 'value'
37+
end
38+
end
39+
threads.each(&:join)
40+
end
41+
end
42+
end
43+
end

spec/rails_admin/config/proxyable/proxy_spec.rb

-46
This file was deleted.
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
require 'spec_helper'
2+
3+
describe RailsAdmin::Config::Proxyable do
4+
class ProxyableTest
5+
include RailsAdmin::Config::Proxyable
6+
7+
def boo
8+
sleep 0.15
9+
bindings[:foo]
10+
end
11+
12+
def qux
13+
'foobar'
14+
end
15+
end
16+
17+
let!(:proxyable_test) { ProxyableTest.new }
18+
subject do
19+
proxyable_test.bindings = {foo: 'bar'}
20+
proxyable_test
21+
end
22+
23+
it 'proxies method calls to @object' do
24+
expect(subject.bindings).to eq foo: 'bar'
25+
end
26+
27+
it 'preserves initially set @bindings' do
28+
expect(subject.with(foo: 'baz').tap(&:qux).bindings).to eq foo: 'bar'
29+
end
30+
31+
context 'when a method is defined in Kernel' do
32+
before do
33+
Kernel.module_eval do
34+
def qux
35+
'quux'
36+
end
37+
end
38+
end
39+
40+
after do
41+
Kernel.module_eval do
42+
undef qux
43+
end
44+
end
45+
46+
it 'proxies calls for the method to @object' do
47+
expect(subject.qux).to eq 'foobar'
48+
end
49+
end
50+
51+
describe 'with parallel execution' do
52+
it 'ensures thread-safety' do
53+
threads = Array.new(2) do |i|
54+
Thread.new do
55+
value = %w(a b)[i]
56+
proxy = proxyable_test.with foo: value
57+
sleep i * 0.1
58+
expect(proxy.boo).to eq value
59+
end
60+
end
61+
threads.each(&:join)
62+
end
63+
end
64+
end

0 commit comments

Comments
 (0)