This article is the 20th day article of GraphQL Advent Calendar 2020. Yesterday was @ marin_a__'s How to use LocalState in Apollo Client x codegen.
Suppose you have a GraphQL schema like this:
type Query {
articles: [Article!]
users: [User!]
}
type Article {
title: String!
}
type User {
avatar: String!
}
Suppose Article, User, and Avatar are separate microservices.
Also assume that the APIs provided by the individual services are:
--Article service returns articles (ArticleServiceClient.get_articles
)
--User service returns users (no avatar, only id) (UserServiceClient.get_users
)
--The Avatar service takes a user id and returns an avatar (AvatarServiceClient.get_avatar (id)
)
At this point, consider resolving a query like this:
query {
articles {
title
}
users {
avatar
}
}
If you write it honestly, it looks like the following.
class ArticleType < GraphQL::Schema::Object
field :title, String, null: false
end
class UserType < GraphQL::Schema::Object
field :avatar, String, null: false
def avatar
AvatarServiceClient.get_avatar(object.id)
end
end
class QueryType < GraphQL::Schema::Object
field :users, [UserType], null: true
field :articles, [ArticleType], null: true
def users
UserServiceClient.get_users
end
def articles
ArticleServiceClient.get_articles
end
end
class TestSchema < GraphQL::Schema
query QueryType
end
Let's say it takes 2 seconds to return users, 3 seconds to return articles, 1 second to return avatar, and if there are 3 users, a total of 2 + 3 + 1 * 3 takes 8 seconds. I will.
However, since articles and users are schema independent, it seems possible to make requests in parallel. GraphQL Ruby has Mechanism for lazy evaluation, which can be combined with concurrent-ruby for parallel processing [^ 1].
Specifically, it can be realized by wrapping the part that makes a request to the service in Concurrent :: Promises.future
and setting it tolazy_resolve
as shown below.
class UserType < GraphQL::Schema::Object
def avatar
Concurrent::Promises.future do
AvatarServiceClient.get_avatar(object.id)
end
end
end
class QueryType < GraphQL::Schema::Object
def users
Concurrent::Promises.future do
UserServiceClient.get_users
end
end
def articles
Concurrent::Promises.future do
ArticleServiceClient.get_articles
end
end
end
class TestSchema < GraphQL::Schema
lazy_resolve Concurrent::Promises::Future, :value!
end
However, GraphQL Ruby's lazy_resolve
seems to wait for all sibling fields.
In this example, users can return articles earlier than 1 second articles, but wait for articles before avatar is evaluated.
This means that if avatar runs immediately after users, it only takes 3 seconds, but it actually takes 4 seconds.
You can check the operation by saving the following code and doing $ ARTICLE_SERVICE_SLEEP = 3 USER_SERVICE_SLEEP = 2 AVATAR_SERVICE_SLEEP = 1 FUTURE = 1 ruby a.rb
. It can be confirmed that the execution time of a little over 8 seconds becomes a little over 4 seconds depending on the presence or absence of FUTURE = 1
.
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "graphql"
gem "concurrent-ruby"
end
query = <<~Q
query {
articles {
title
}
users {
avatar
}
}
Q
require 'logger'
$logger = Logger.new(STDOUT)
module ArticleServiceClient
Article = Struct.new(:id, :title, keyword_init: true)
ALL_ARTICLES = Array.new(3) do |i|
Article.new(id: i, title: "title#{i}")
end
def self.get_articles
$logger.debug("#{name}##{__method__} start")
sleep ENV['ARTICLE_SERVICE_SLEEP'].to_i
$logger.debug("#{name}##{__method__} end")
ALL_ARTICLES
end
end
module UserServiceClient
User = Struct.new(:id, keyword_init: true)
ALL_USERS = Array.new(3) do |i|
User.new(id: i)
end
def self.get_users
$logger.debug("#{name}##{__method__} start")
sleep ENV['USER_SERVICE_SLEEP'].to_i
$logger.debug("#{name}##{__method__} end")
ALL_USERS
end
end
module AvatarServiceClient
def self.get_avatar(id)
$logger.debug("#{name}##{__method__} start")
sleep ENV['AVATAR_SERVICE_SLEEP'].to_i
$logger.debug("#{name}##{__method__} end")
"#{id}.jpg "
end
end
class ArticleType < GraphQL::Schema::Object
field :title, String, null: false
def title
$logger.debug("#{self.class}##{__method__}")
object.title
end
end
class UserType < GraphQL::Schema::Object
field :avatar, String, null: false
def avatar
if ENV['FUTURE']
Concurrent::Promises.future do
AvatarServiceClient.get_avatar(object.id)
end
else
AvatarServiceClient.get_avatar(object.id)
end
end
end
class QueryType < GraphQL::Schema::Object
field :articles, [ArticleType], null: true
field :users, [UserType], null: true
def articles
if ENV['FUTURE']
Concurrent::Promises.future do
ArticleServiceClient.get_articles
end
else
ArticleServiceClient.get_articles
end
end
def users
if ENV['FUTURE']
Concurrent::Promises.future do
UserServiceClient.get_users
end
else
UserServiceClient.get_users
end
end
end
class TestSchema < GraphQL::Schema
query QueryType
lazy_resolve Concurrent::Promises::Future, :value!
end
pp TestSchema.execute(query).to_h
__END__
$ time ARTICLE_SERVICE_SLEEP=3 USER_SERVICE_SLEEP=2 AVATAR_SERVICE_SLEEP=1 ruby a.rb
D, [2020-12-20T12:11:14.873364 #23151] DEBUG -- : ArticleServiceClient#get_articles start
D, [2020-12-20T12:11:17.876517 #23151] DEBUG -- : ArticleServiceClient#get_articles end
D, [2020-12-20T12:11:17.876831 #23151] DEBUG -- : ArticleType#title
D, [2020-12-20T12:11:17.876937 #23151] DEBUG -- : ArticleType#title
D, [2020-12-20T12:11:17.877046 #23151] DEBUG -- : ArticleType#title
D, [2020-12-20T12:11:17.877143 #23151] DEBUG -- : UserServiceClient#get_users start
D, [2020-12-20T12:11:19.879268 #23151] DEBUG -- : UserServiceClient#get_users end
D, [2020-12-20T12:11:19.879546 #23151] DEBUG -- : AvatarServiceClient#get_avatar start
D, [2020-12-20T12:11:20.880675 #23151] DEBUG -- : AvatarServiceClient#get_avatar end
D, [2020-12-20T12:11:20.880875 #23151] DEBUG -- : AvatarServiceClient#get_avatar start
D, [2020-12-20T12:11:21.882006 #23151] DEBUG -- : AvatarServiceClient#get_avatar end
D, [2020-12-20T12:11:21.882193 #23151] DEBUG -- : AvatarServiceClient#get_avatar start
D, [2020-12-20T12:11:22.883306 #23151] DEBUG -- : AvatarServiceClient#get_avatar end
{"data"=>
{"articles"=>[{"title"=>"title0"}, {"title"=>"title1"}, {"title"=>"title2"}],
"users"=>[{"avatar"=>"0.jpg "}, {"avatar"=>"1.jpg "}, {"avatar"=>"2.jpg "}]}}
ARTICLE_SERVICE_SLEEP=3 USER_SERVICE_SLEEP=2 AVATAR_SERVICE_SLEEP=1 ruby a.rb 0.26s user 0.02s system 3% cpu 8.286 total
$ time ARTICLE_SERVICE_SLEEP=3 USER_SERVICE_SLEEP=2 AVATAR_SERVICE_SLEEP=1 FUTURE=1 ruby a.rb
D, [2020-12-20T12:11:37.120206 #23200] DEBUG -- : ArticleServiceClient#get_articles start
D, [2020-12-20T12:11:37.120294 #23200] DEBUG -- : UserServiceClient#get_users start
D, [2020-12-20T12:11:39.120599 #23200] DEBUG -- : UserServiceClient#get_users end
D, [2020-12-20T12:11:40.120562 #23200] DEBUG -- : ArticleServiceClient#get_articles end
D, [2020-12-20T12:11:40.120985 #23200] DEBUG -- : ArticleType#title
D, [2020-12-20T12:11:40.121188 #23200] DEBUG -- : ArticleType#title
D, [2020-12-20T12:11:40.121261 #23200] DEBUG -- : ArticleType#title
D, [2020-12-20T12:11:40.121934 #23200] DEBUG -- : AvatarServiceClient#get_avatar start
D, [2020-12-20T12:11:40.121994 #23200] DEBUG -- : AvatarServiceClient#get_avatar start
D, [2020-12-20T12:11:40.122088 #23200] DEBUG -- : AvatarServiceClient#get_avatar start
D, [2020-12-20T12:11:41.122276 #23200] DEBUG -- : AvatarServiceClient#get_avatar end
D, [2020-12-20T12:11:41.122413 #23200] DEBUG -- : AvatarServiceClient#get_avatar end
D, [2020-12-20T12:11:41.122499 #23200] DEBUG -- : AvatarServiceClient#get_avatar end
{"data"=>
{"articles"=>[{"title"=>"title0"}, {"title"=>"title1"}, {"title"=>"title2"}],
"users"=>[{"avatar"=>"0.jpg "}, {"avatar"=>"1.jpg "}, {"avatar"=>"2.jpg "}]}}
ARTICLE_SERVICE_SLEEP=3 USER_SERVICE_SLEEP=2 AVATAR_SERVICE_SLEEP=1 FUTURE=1 0.22s user 0.07s system 6% cpu 4.285 total
[^ 1]: Since Ruby Thread is used, it is not parallel but parallel at Ruby level due to GVL. However, in the case of waiting for IO, GVL is released, so inter-service communication like this one is performed in parallel. https://docs.ruby-lang.org/ja/latest/doc/spec=2fthread.html
Recommended Posts