Skip to content

Commit c91fe70

Browse files
committed
MONGOID-5882 isolation state
1 parent fe9397e commit c91fe70

File tree

3 files changed

+164
-31
lines changed

3 files changed

+164
-31
lines changed

lib/mongoid/config.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,19 @@ module Config
110110
# to `:global_thread_pool`.
111111
option :global_executor_concurrency, default: nil
112112

113+
# Defines the isolation level that Mongoid uses to store its internal
114+
# state.
115+
#
116+
# This option may be set to either `:thread` (the default) or `:fiber`.
117+
#
118+
# If set to `:thread`, Mongoid will use thread-local storage to manage
119+
# its internal state.
120+
#
121+
# If set to `:fiber`, Mongoid will use fiber-local storage instead. This
122+
# may be necessary if you are using libraries like Falcon, which use
123+
# fibers to manage concurrency.
124+
option :isolation_level, default: :thread
125+
113126
# When this flag is false, a document will become read-only only once the
114127
# #readonly! method is called, and an error will be raised on attempting
115128
# to save or update such documents, instead of just on delete. When this

lib/mongoid/threaded.rb

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,56 @@ module Mongoid
66
# This module contains logic for easy access to objects that have a lifecycle
77
# on the current thread.
88
module Threaded
9-
DATABASE_OVERRIDE_KEY = '[mongoid]:db-override'
9+
# The key for the shared thread- and fiber-local storage. It must be a
10+
# symbol because keys for fiber-local storage must be symbols.
11+
STORAGE_KEY = :'[mongoid]'
1012

11-
# Constant for the key to store clients.
12-
CLIENTS_KEY = '[mongoid]:clients'
13+
DATABASE_OVERRIDE_KEY = 'db-override'
1314

1415
# The key to override the client.
15-
CLIENT_OVERRIDE_KEY = '[mongoid]:client-override'
16+
CLIENT_OVERRIDE_KEY = 'client-override'
1617

1718
# The key for the current thread's scope stack.
18-
CURRENT_SCOPE_KEY = '[mongoid]:current-scope'
19+
CURRENT_SCOPE_KEY = 'current-scope'
1920

20-
AUTOSAVES_KEY = '[mongoid]:autosaves'
21+
AUTOSAVES_KEY = 'autosaves'
2122

22-
VALIDATIONS_KEY = '[mongoid]:validations'
23+
VALIDATIONS_KEY = 'validations'
2324

2425
STACK_KEYS = Hash.new do |hash, key|
25-
hash[key] = "[mongoid]:#{key}-stack"
26+
hash[key] = "#{key}-stack"
2627
end
2728

2829
# The key for the current thread's sessions.
29-
SESSIONS_KEY = '[mongoid]:sessions'
30+
SESSIONS_KEY = 'sessions'
3031

3132
# The key for storing documents modified inside transactions.
32-
MODIFIED_DOCUMENTS_KEY = '[mongoid]:modified-documents'
33+
MODIFIED_DOCUMENTS_KEY = 'modified-documents'
3334

3435
# The key storing the default value for whether or not callbacks are
3536
# executed on documents.
36-
EXECUTE_CALLBACKS = '[mongoid]:execute-callbacks'
37+
EXECUTE_CALLBACKS = 'execute-callbacks'
3738

3839
extend self
3940

40-
# Queries the thread-local variable with the given name. If a block is
41+
# Resets the current thread- or fiber-local storage to its initial state.
42+
# This is useful for making sure the state is clean when starting a new
43+
# thread or fiber.
44+
#
45+
# The value of Mongoid::Config.isolation_level is used to determine
46+
# whether to reset the storage for the current thread or fiber.
47+
def reset!
48+
case Config.isolation_level
49+
when :thread
50+
Thread.current.thread_variable_set(STORAGE_KEY, nil)
51+
when :fiber
52+
Fiber[STORAGE_KEY] = nil
53+
else
54+
raise "Unknown isolation level: #{Config.isolation_level.inspect}"
55+
end
56+
end
57+
58+
# Queries the thread- or fiber-local variable with the given name. If a block is
4159
# given, and the variable does not already exist, the return value of the
4260
# block will be set as the value of the variable before returning it.
4361
#
@@ -57,7 +75,7 @@ module Threaded
5775
# @return [ Object | nil ] the value of the queried variable, or nil if
5876
# it is not set and no default was given.
5977
def get(key, &default)
60-
result = Thread.current.thread_variable_get(key)
78+
result = storage[key]
6179

6280
if result.nil? && default
6381
result = yield
@@ -67,43 +85,31 @@ def get(key, &default)
6785
result
6886
end
6987

70-
# Sets a thread-local variable with the given name to the given value.
88+
# Sets a variable in local storage with the given name to the given value.
7189
# See #get for a discussion of why this method is necessary, and why
7290
# Thread#[]= should be avoided in cascading callbacks on embedded children.
7391
#
7492
# @param [ String | Symbol ] key the name of the variable to set.
7593
# @param [ Object | nil ] value the value of the variable to set (or `nil`
7694
# if you wish to unset the variable)
7795
def set(key, value)
78-
Thread.current.thread_variable_set(key, value)
96+
storage[key] = value
7997
end
8098

81-
# Removes the named variable from thread-local storage.
99+
# Removes the named variable from local storage.
82100
#
83101
# @param [ String | Symbol ] key the name of the variable to remove.
84102
def delete(key)
85-
set(key, nil)
103+
storage.delete(key)
86104
end
87105

88-
# Queries the presence of a named variable in thread-local storage.
106+
# Queries the presence of a named variable in local storage.
89107
#
90108
# @param [ String | Symbol ] key the name of the variable to query.
91109
#
92110
# @return [ true | false ] whether the given variable is present or not.
93111
def has?(key)
94-
# Here we have a classic example of JRuby not behaving like MRI. In
95-
# MRI, if you set a thread variable to nil, it removes it from the list
96-
# and subsequent calls to thread_variable?(key) will return false. Not
97-
# so with JRuby. Once set, you cannot unset the thread variable.
98-
#
99-
# However, because setting a variable to nil is supposed to remove it,
100-
# we can assume a nil-valued variable doesn't actually exist.
101-
102-
# So, instead of this:
103-
# Thread.current.thread_variable?(key)
104-
105-
# We have to do this:
106-
!get(key).nil?
112+
storage.key?(key)
107113
end
108114

109115
# Begin entry into a named thread local stack.
@@ -508,5 +514,22 @@ def unset_current_scope(klass)
508514

509515
delete(CURRENT_SCOPE_KEY) if scope.empty?
510516
end
517+
518+
# Returns the current thread- or fiber-local storage as a Hash.
519+
def storage
520+
case Config.isolation_level
521+
when :thread
522+
if !Thread.current.thread_variable?(STORAGE_KEY)
523+
Thread.current.thread_variable_set(STORAGE_KEY, {})
524+
end
525+
526+
Thread.current.thread_variable_get(STORAGE_KEY)
527+
when :fiber
528+
Fiber[STORAGE_KEY] ||= {}
529+
else
530+
raise "Unknown isolation level: #{Config.isolation_level.inspect}"
531+
end
532+
end
533+
511534
end
512535
end
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# frozen_string_literal: true
2+
# rubocop:todo all
3+
4+
require 'spec_helper'
5+
6+
describe 'Mongoid::Config.isolation_level' do
7+
def thread_operation(value)
8+
Thread.new do
9+
Mongoid::Threaded.stack(:testing) << value
10+
yield if block_given?
11+
Mongoid::Threaded.stack(:testing)
12+
end.join.value
13+
end
14+
15+
def fiber_operation(value)
16+
Fiber.new do
17+
Mongoid::Threaded.stack(:testing) << value
18+
yield if block_given?
19+
Mongoid::Threaded.stack(:testing)
20+
end.resume
21+
end
22+
23+
context 'when set to :thread' do
24+
config_override :isolation_level, :thread
25+
26+
context 'when not operating inside fibers' do
27+
let(:result1) { thread_operation('a') { thread_operation('b') } }
28+
let(:result2) { thread_operation('b') { thread_operation('c') } }
29+
30+
it 'isolates state per thread' do
31+
expect(result1).to eq(%w[ a ])
32+
expect(result2).to eq(%w[ b ])
33+
end
34+
end
35+
36+
context 'when operating inside fibers' do
37+
let(:result) { thread_operation('a') { fiber_operation('b') } }
38+
39+
it 'exposes the thread state within the fiber' do
40+
expect(result).to eq(%w[ a b ])
41+
end
42+
end
43+
end
44+
45+
context 'when set to :fiber' do
46+
config_override :isolation_level, :fiber
47+
48+
context 'when operating inside threads' do
49+
let(:result) { fiber_operation('a') { thread_operation('b') } }
50+
51+
it 'exposes the fiber state within the thread' do
52+
expect(result).to eq(%w[ a b ])
53+
end
54+
end
55+
56+
context 'when operating in nested fibers' do
57+
let(:result) { fiber_operation('a') { fiber_operation('b') } }
58+
59+
it 'propagates fiber state to nested fibers' do
60+
expect(result).to eq(%w[ a b ])
61+
end
62+
end
63+
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') } }
67+
68+
it 'maintains isolation between adjacent fibers' do
69+
expect(result1).to eq(%w[ a b ])
70+
expect(result2).to eq(%w[ c d ])
71+
end
72+
end
73+
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'
88+
end
89+
end
90+
91+
it 'clears the fiber state' do
92+
expect(result).to eq(%w[ d ])
93+
end
94+
end
95+
end
96+
end
97+
end

0 commit comments

Comments
 (0)