Skip to content

Commit 2e9e8e4

Browse files
author
Robert Mosolgo
authored
Merge pull request #3461 from ianks/fiber-thread-locals
Copy parent thread local vars to dataloader fiber local vars
2 parents 7eae696 + 856a50b commit 2e9e8e4

File tree

3 files changed

+73
-3
lines changed

3 files changed

+73
-3
lines changed

guides/dataloader/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ At a high level, `GraphQL::Dataloader`'s usage of `Fiber` looks like this:
3333
- `GraphQL::Dataloader` takes the first paused Fiber and resumes it, causing the `GraphQL::Dataloader::Source` to execute its `#fetch(...)` call. That Fiber continues execution as far as it can.
3434
- Likewise, paused Fibers are resumed, causing GraphQL execution to continue, until all paused Fibers are evaluated completely.
3535

36-
Since _all_ GraphQL execution is inside a Fiber, __`Thread.current[...]` won't be set__. Those assignments are _Fiber-local_, so each new Fiber has an _empty_ `Thread.current`. For `GraphQL::Dataloader`, use GraphQL-Ruby's `context` object to provide "current" values to a GraphQL query. Inside `#fetch(...)` methods, those values can be re-assigned to `Thread.current` if application code requires it.
36+
Whenever `GraphQL::Dataloader` creates a new `Fiber`, it copies each pair from `Thread.current[...]` and reassigns them inside the new `Fiber`.
3737

3838
## Getting Started
3939

lib/graphql/dataloader.rb

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def run
116116
while @pending_jobs.any?
117117
# Create a Fiber to consume jobs until one of the jobs yields
118118
# or jobs run out
119-
f = Fiber.new {
119+
f = spawn_fiber {
120120
while (job = @pending_jobs.shift)
121121
job.call
122122
end
@@ -203,7 +203,7 @@ def create_source_fiber
203203
#
204204
# This design could probably be improved by maintaining a `@pending_sources` queue which is shared by the fibers,
205205
# similar to `@pending_jobs`. That way, when a fiber is resumed, it would never pick up work that was finished by a different fiber.
206-
source_fiber = Fiber.new do
206+
source_fiber = spawn_fiber do
207207
pending_sources.each(&:run_pending_keys)
208208
end
209209
end
@@ -216,5 +216,24 @@ def resume(fiber)
216216
rescue UncaughtThrowError => e
217217
throw e.tag, e.value
218218
end
219+
220+
# Copies the thread local vars into the fiber thread local vars. Many
221+
# gems (such as RequestStore, MiniRacer, etc.) rely on thread local vars
222+
# to keep track of execution context, and without this they do not
223+
# behave as expected.
224+
#
225+
# @see https://github.com/rmosolgo/graphql-ruby/issues/3449
226+
def spawn_fiber
227+
fiber_locals = {}
228+
229+
Thread.current.keys.each do |fiber_var_key|
230+
fiber_locals[fiber_var_key] = Thread.current[fiber_var_key]
231+
end
232+
233+
Fiber.new do
234+
fiber_locals.each { |k, v| Thread.current[k] = v }
235+
yield
236+
end
237+
end
219238
end
220239
end

spec/graphql/dataloader_spec.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,4 +733,55 @@ def request_all
733733

734734
assert :world, value
735735
end
736+
737+
describe "thread local variables" do
738+
module ThreadVariable
739+
class Type < GraphQL::Schema::Object
740+
field :key, String, null: false
741+
field :value, String, null: false
742+
end
743+
744+
class Source < GraphQL::Dataloader::Source
745+
def fetch(keys)
746+
keys.map { |key| OpenStruct.new(key: key, value: Thread.current[key.to_sym]) }
747+
end
748+
end
749+
750+
class QueryType < GraphQL::Schema::Object
751+
field :thread_var, ThreadVariable::Type, null: true do
752+
argument :key, GraphQL::Types::String, required: true
753+
end
754+
755+
def thread_var(key:)
756+
dataloader.with(ThreadVariable::Source).load(key)
757+
end
758+
end
759+
760+
class Schema < GraphQL::Schema
761+
query ThreadVariable::QueryType
762+
use GraphQL::Dataloader
763+
end
764+
end
765+
766+
it "sets the parent thread locals in the execution fiber" do
767+
Thread.current[:test_thread_var] = 'foobarbaz'
768+
769+
result = ThreadVariable::Schema.execute(<<-GRAPHQL)
770+
{
771+
threadVar(key: "test_thread_var") {
772+
key
773+
value
774+
}
775+
}
776+
GRAPHQL
777+
778+
expected_result = {
779+
"data" => {
780+
"threadVar" => { "key" => "test_thread_var", "value" => "foobarbaz" }
781+
}
782+
}
783+
784+
assert_equal expected_result, result.to_h
785+
end
786+
end
736787
end

0 commit comments

Comments
 (0)