Advanced Graphql — Subscription Performance

Han
4 min readMar 17, 2022

--

Problem Statement

For every broadcast, Subscription Query is executed once per each client. A large server performance would be dramatically impacted by this.

Graphql Subscriptions works as a listener to wait on server to push data instead of periodically polling the server. Besides Graphql subscriptions, there are many other existing solutions that can “listen” to server. Old fashioned websocket, ActionCable (Ruby on Rails), etc. One of the biggest difference between Graphql Subscription and others is that Graphql subscription subscript be one event, but the the payload can vary for each client. For example, image a subscription definition:

resolvers:

Each client could subscribe to a subset of these fields and nested fields. Unlike regular pubsub + websocket framework, where the all clients receive the same broadcast message in a channel, in Graphql subscription, each client needs to call resolve to refetch the exact data when the broadcast happened. (see "Execute Subscription Query" below)

(credit: https://dgraph.io/blog/post/how-does-graphql-subscription/).

This immediately brings up a problem, for each broadcast, the resolve function is called N times, where N is the number of subscribed client. This has a significant impact as the number of subscription client increases.

Solution 1. Dataloader

Now, if you use some kind of dataloader mechanism that is globally available, then this may not be a big problem. Because even though the resolver is called N times, they maybe merged in one single dataloader and endup with only 1 database visit.

Solution 2. Prisma

Or if you use prisma client, where prisma is globally availabe, and you programed the resolve function carefully, this problem may also go away.

More Problem: What if you use PostGraphile

One problem I have ran into in particular, is PostGraphile’s subscription. If you don’t know about it yet, check it out here. Basically this library generate the a graphql schema from a postgreSQL schema, it use the index/foreign key to create assications, also it uses a “look ahead” to convert a large GraphQL query into a complex SQL query and run it once to retrieve all the data. It can also be directly invoked as a server with jwt, rbac etc. Because of this feature, with PostGraphile, you don’t need to manually setup prisma client, datasources, or any kind of ORM for the resolvers. PostGraphile’s subscription cleverly uses Postgres’s Pubsub (so you don’t need to run a redis) as the broker and uses the query function pg_notify as trigger to trigger subscriptions. see more information here.

One example of how you can set up subscription with PostGraphile is like following.

The selectGraphQLResultFromTable embed the "look ahead" technique and construct a complex sql to retrieve the data. If a client subscribe to server:

Inside the seleceGraphQLResultFromTable it would construct an SQL like this:

The exact query will involve some alias and cte, but you get the idea. However, this look ahead helps to retrieve data with 1 single query per client, but we still cannot resolve the problem that the resolver is called once for each client. So we still need N database visit per broadcast.

Solution for PostGraphile subscription.

The solution for this problem is to not use the dataloader “vertically”. Let me explain. DataLoader is used to batch request to a single request, and split up the result for each individual request. Like so

Imagine this is a “horizontal way of batching”, there is also a “vertical” batching. Like so:

In Graphql Document, the “fields” are stored in the info(4th parameter when calling resolver), access by info.fieldNodes[0] so we in pseudocode, we should do this:

The merge function is a recursive function that just manually merge all the selectionSet in a graphql Document. I wrote one myself and it works pretty well, but not guarantee to be bug free.

Now let’s talk about the magicResolve. it is pretty hard to figure out how PostGraphile does the look up or find a utility existing function to match our need. The exposed selectGraphQLResultFromTable hides all the secrets from the user. So instead of figuring all these out, I uses delegateToSchema from @graphql-tools/delegate, I basically construct a graphql query like below and execute the query with the schema.

In practise, there are a lot of details to sort out, for example, where do these context, schema come from ? From our experiment, the PostGraphile context for query only needs a pgClient to function correctly, so we could just simply construct a context inside the dataloader. schema should be the same for all queries, so we could set the schema when we create these dataloaders. Or we just pick schema from any info.

For a full repo to demostrate this, check out this repo

Originally published at http://github.com.Problem Statement

--

--

Han
Han

Written by Han

Google SWE | Newly Dad | Computational Biology PhD | Home Automation Enthusiast

No responses yet