Skip to content

Commit f93cb4e

Browse files
committed
:fiber level only works reliably on Ruby 3.2+
1 parent c91fe70 commit f93cb4e

File tree

6 files changed

+132
-38
lines changed

6 files changed

+132
-38
lines changed

lib/config/locales/en.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,16 @@ en:
712712
the expression %{javascript} is not allowed."
713713
resolution: "Please provide a standard hash to #where when the criteria
714714
is for an embedded association."
715+
unsupported_isolation_level:
716+
message: "The isolation level '%{level}' is not supported."
717+
summary: >
718+
You requested an isolation level of '%{level}', which is not
719+
supported. Only `:thread` and `:fiber` isolation levels are
720+
currently supported; note that the `:fiber` level is only
721+
supported on Ruby versions 3.2 and higher.
722+
resolution: >
723+
Use `:thread` as the isolation level. If you are using Ruby 3.2
724+
or higher, you may also use `:fiber`.
715725
validations:
716726
message: "Validation of %{document} failed."
717727
summary: "The following errors were found: %{errors}"

lib/mongoid/config.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,19 @@ module Config
121121
# If set to `:fiber`, Mongoid will use fiber-local storage instead. This
122122
# may be necessary if you are using libraries like Falcon, which use
123123
# fibers to manage concurrency.
124-
option :isolation_level, default: :thread
124+
#
125+
# Note that the `:fiber` isolation level is only supported in Ruby 3.2
126+
# and later, due to semantic differences in how fiber storage is handled
127+
# in earlier Ruby versions.
128+
option :isolation_level, default: :thread, on_change: -> (level) do
129+
if %i[ thread fiber ].exclude?(level)
130+
raise Errors::UnsupportedIsolationLevel.new(level)
131+
end
132+
133+
if level == :fiber && RUBY_VERSION < '3.2'
134+
raise Errors::UnsupportedIsolationLevel.new(level)
135+
end
136+
end
125137

126138
# When this flag is false, a document will become read-only only once the
127139
# #readonly! method is called, and an error will be raised on attempting

lib/mongoid/config/options.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,17 @@ def option(name, options = {})
4040
end
4141

4242
define_method("#{name}=") do |value|
43+
old_value = settings[name]
4344
settings[name] = value
44-
options[:on_change]&.call(value)
45+
46+
begin
47+
options[:on_change]&.call(value)
48+
rescue
49+
# If the on_change callback raises an error, we need to roll
50+
# the change back.
51+
settings[name] = old_value
52+
raise
53+
end
4554
end
4655

4756
define_method("#{name}?") do

lib/mongoid/errors.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,6 @@
7474
require 'mongoid/errors/unregistered_class'
7575
require "mongoid/errors/unsaved_document"
7676
require "mongoid/errors/unsupported_javascript"
77+
require "mongoid/errors/unsupported_isolation_level"
7778
require "mongoid/errors/validations"
7879
require "mongoid/errors/delete_restriction"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Errors
5+
6+
# Raised when an unsupported isolation level is used in Mongoid
7+
# configuration.
8+
class UnsupportedIsolationLevel < MongoidError
9+
# Create the new error caused by attempting to select an unsupported
10+
# isolation level.
11+
#
12+
# @param [ Symbol ] level The requested isolation level.
13+
def initialize(level)
14+
super(
15+
compose_message(
16+
"unsupported_isolation_level",
17+
{ level: level }
18+
)
19+
)
20+
end
21+
end
22+
end
23+
end

spec/integration/isolation_state_spec.rb

Lines changed: 75 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,41 @@ def fiber_operation(value)
2020
end.resume
2121
end
2222

23+
context 'when set to an unsupported value' do
24+
it 'raises an error' do
25+
old_value = Mongoid::Config.isolation_level
26+
expect { Mongoid::Config.isolation_level = :unsupported }
27+
.to raise_error(Mongoid::Errors::UnsupportedIsolationLevel)
28+
expect(Mongoid::Config.isolation_level).to eq(old_value)
29+
end
30+
end
31+
32+
context 'when using older Ruby' do
33+
ruby_version_lt '3.2'
34+
35+
context 'when set to :fiber' do
36+
it 'raises an error' do
37+
expect { Mongoid::Config.isolation_level = :fiber }
38+
.to raise_error(Mongoid::Errors::UnsupportedIsolationLevel)
39+
end
40+
end
41+
42+
context 'when set to :thread' do
43+
around do |example|
44+
save = Mongoid::Config.isolation_level
45+
example.run
46+
ensure
47+
Mongoid::Config.isolation_level = save
48+
end
49+
50+
it 'sets the isolation level' do
51+
expect { Mongoid::Config.isolation_level = :thread }
52+
.not_to raise_error
53+
expect(Mongoid::Config.isolation_level).to eq(:thread)
54+
end
55+
end
56+
end
57+
2358
context 'when set to :thread' do
2459
config_override :isolation_level, :thread
2560

@@ -42,54 +77,58 @@ def fiber_operation(value)
4277
end
4378
end
4479

45-
context 'when set to :fiber' do
46-
config_override :isolation_level, :fiber
80+
context 'when using Ruby 3.2+' do
81+
ruby_version_gte '3.2'
4782

48-
context 'when operating inside threads' do
49-
let(:result) { fiber_operation('a') { thread_operation('b') } }
83+
context 'when set to :fiber' do
84+
config_override :isolation_level, :fiber
5085

51-
it 'exposes the fiber state within the thread' do
52-
expect(result).to eq(%w[ a b ])
86+
context 'when operating inside threads' do
87+
let(:result) { fiber_operation('a') { thread_operation('b') } }
88+
89+
it 'exposes the fiber state within the thread' do
90+
expect(result).to eq(%w[ a b ])
91+
end
5392
end
54-
end
5593

56-
context 'when operating in nested fibers' do
57-
let(:result) { fiber_operation('a') { fiber_operation('b') } }
94+
context 'when operating in nested fibers' do
95+
let(:result) { fiber_operation('a') { fiber_operation('b') } }
5896

59-
it 'propagates fiber state to nested fibers' do
60-
expect(result).to eq(%w[ a b ])
97+
it 'propagates fiber state to nested fibers' do
98+
expect(result).to eq(%w[ a b ])
99+
end
61100
end
62-
end
63101

64-
context 'when operating in adjacent fibers' do
65-
let(:result1) { fiber_operation('a') { fiber_operation('b') } }
66-
let(:result2) { fiber_operation('c') { fiber_operation('d') } }
102+
context 'when operating in adjacent fibers' do
103+
let(:result1) { fiber_operation('a') { fiber_operation('b') } }
104+
let(:result2) { fiber_operation('c') { fiber_operation('d') } }
67105

68-
it 'maintains isolation between adjacent fibers' do
69-
expect(result1).to eq(%w[ a b ])
70-
expect(result2).to eq(%w[ c d ])
106+
it 'maintains isolation between adjacent fibers' do
107+
expect(result1).to eq(%w[ a b ])
108+
expect(result2).to eq(%w[ c d ])
109+
end
71110
end
72-
end
73111

74-
describe '#reset!' do
75-
context 'when operating in nested fibers' do
76-
let (:result) do
77-
fiber_operation('a') do
78-
Mongoid::Threaded.reset!
79-
80-
# once reset, subsequent nested fibers will each have their own
81-
# state; they won't touch the reset state here.
82-
fiber_operation('b')
83-
fiber_operation('c')
84-
85-
# If we then add to the stack here, it will be unaffected by
86-
# the previous fiber operations.
87-
Mongoid::Threaded.stack(:testing) << 'd'
112+
describe '#reset!' do
113+
context 'when operating in nested fibers' do
114+
let (:result) do
115+
fiber_operation('a') do
116+
Mongoid::Threaded.reset!
117+
118+
# once reset, subsequent nested fibers will each have their own
119+
# state; they won't touch the reset state here.
120+
fiber_operation('b')
121+
fiber_operation('c')
122+
123+
# If we then add to the stack here, it will be unaffected by
124+
# the previous fiber operations.
125+
Mongoid::Threaded.stack(:testing) << 'd'
126+
end
88127
end
89-
end
90128

91-
it 'clears the fiber state' do
92-
expect(result).to eq(%w[ d ])
129+
it 'clears the fiber state' do
130+
expect(result).to eq(%w[ d ])
131+
end
93132
end
94133
end
95134
end

0 commit comments

Comments
 (0)