-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
use GraphQL::Dataloader
breaks RequestStore usage
#3449
Comments
Hey, thanks for the detailed report.
Yes, I bet that's exactly it. When Dataloader is enabled, all GraphQL execution takes place inside a Fiber. As you noticed, My first recommendation is to use context = {
current_tenant: ActsAsTenant.current_tenant
# ...
}
MySchema.execute(query_str, variables: variables, context: context) To your GraphQL controller. Then, inside fields that require a def catalog(id:)
ActsAsTenant.with_tenant(context[:current_tenant]) do
Catalog.find!(id)
end
end If a lot of your fields require a Let me know if the |
Thanks for the fast response! Yes, indeed all models are tied to an account, so all fields need to now about the current tenant (Ok, a catalog with a specific id does not need the account id, but connections like
class MySchema < GraphQL::Schema
use GraphQL::Subscriptions::ActionCableSubscriptions
use GraphQL::Dataloader
query Types::Query
mutation Types::Mutation
subscription Types::Subscription
self.before_execution
ActsAsTenant.current_tenant = context[:current_tenant]
end
end Would something like this makes sense? |
Something like that might work for now (see "query instrumenters"), but the problem is, with Dataloader, GraphQL-Ruby will spin up new Fibers as it needs them. Those newly-created Fibers won't have the context that they need to use Here's what I recommend:
class CurrentTenantExtension < GraphQL::Schema::FieldExtension
# Make sure that `current_tenant` is available for field resolution, then continue with resolution as usual.
def resolve(object:, arguments:, context:, **_rest)
ActsAsTenant.current_tenant = context[:current_tenant]
yield(object, arguments)
end
end Then, you could add this extension whenever you want, eg: field :catalog, Types::Catalog, extensions: [CurrentTenantExtension]
class BaseField < GraphQL::Schema::Field
def initialize(*args, **kwargs, &block)
super
if type.unwrap.kind.composite? # This is true for Object, Interface, and Union types
extension(CurrentTenantExtension)
end
end
end If there are scalar or enum fields that also require that context, you could add the extension as-needed to those fields, or remove the
class ActiveRecordSource < GraphQL::Dataloader::Source
def initialize(tenant, model_class)
@tenant = tenant
@model_class = model_class
end
def fetch(ids)
ActsAsTenant.with_tenant(@tenant) do
@model_class.where(id: ids)
end
end
end Then, in fields, you'd pass the tenant when loading objects with dataloader: def catalog(id:)
dataloader.with(ActsAsTenant.current_tenant, Catalog).load(id)
end (You could probably extract the tenant handling as you see fit -- I think you could even assign What do you think of an approach like that? Let me know how it goes if you give it a try! |
Holy moly, thanks again! I'll have a look at this later, since I've just hacked a bit around at the same time and was happy that my misuse of a tracer worked 😄 class CurrentTenant
def self.use(schema_defn, options = {})
tracer = new(**options)
schema_defn.instrument(:field, tracer) unless schema_defn.is_a?(Class)
schema_defn.tracer(tracer)
end
def trace(key, data)
ActsAsTenant.current_tenant = data[:context][:current_tenant] if data[:context] && data[:context][:current_tenant]
yield
end
end
Hmmm, is there a way to reproduce this? Am I able to spin up more Fibers by ... creating a very large in a test case, for example? |
Ok, great, I've just adopted your solution and it works fine! Thanks for your fast support! |
@rmosolgo I just wanted to add that using |
What's |
|
The only definition I could find for Could you check where your p method(:current_user).source_location ? (I can't figure out how a controller helper would have ever worked inside GraphQL!) |
Hmmm this one does indeed not work inside any of my GraphQL code. But this flow worked before adding the
|
It looks like |
This also breaks Rack::MiniRacer for us. Unfortunately, this seems like a deal breaker for use. Do you think it would make sense to copy over thread locals to the fiber in this case? it seems like that would be the least disruptive policy, although not ideal |
I'm open to it, for sure, I think you could do it wherever Dataloader calls fiber_locals = {}
Thread.current.keys.each do |fiber_var_key|
fiber_locals[fiber_var_key] = Thread.current[fiber_var_key]
end
Fiber.new do
fiber_locals.each { |k, v| Thread.current[k] = v }
# ...
end Is that about what you have in mind? I'm game to give it a shot if it seems reasonable to you. |
Describe the bug
I'm using the gem
acts_as_tenant
which usesRequestStore
to store global values (like the current tenant). But addinguse GraphQL::Dataloader
to the schema breaks this, becauseActsAsTenant.current_tenant
is not defined then:returns just two rows of
###
😄I don't know if this is something that has to be done in the RequestStore gem? Or a bug in
graphql-ruby
itself? Or are both libraries just incompatible and there is no way to resolve this?I am not sure how Dataloader works, but since it's somewhat coupled to Fiber (?) it maybe have to do with how RequestStore stores its data: in Thread.current objects.
Versions
graphql
version: 1.12.8graphql-pro
version: 1.17.14rails
(or other framework): 6.0.3The text was updated successfully, but these errors were encountered: