Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Perl でも GraphQL を使いたい

nakaokat
September 08, 2022

Perl でも GraphQL を使いたい

日付:2022/09/07
イベント名:Hatena Engineer Seminar #21
動画:https://youtu.be/N6iUlz4buTc?t=963

nakaokat

September 08, 2022
Tweet

Other Decks in Programming

Transcript

  1. Perl でも GraphQL を使いたい id:nakaoka3 / @nakaokat 2022/9/7 はてなエンジニアセミナー #21

    1
  2. 自己紹介 • はてなのマンガメディア開発チーム所属のアプリケーションエンジニ ア • コミック DAYS アプリなどスマートフォンアプリの開発を経て、現在 は GigaViewer

    のサーバーサイドの開発をしている 2
  3. アジェンダ • GigaViewer でも GraphQL を使いたい • Perl でも GraphQL

    を使いたい • GraphQL でもキャッシュを使いたい 3
  4. GigaViewer でも GraphQL を使いたい 4

  5. GigaViewer とは > 当社では「はてなブログ」や「はてなブックマーク」などの個人向け Web サービスの提供で培った技 術力を活かし、2017 年に Web サイトとしての魅力を引き出せるマンガビューワである「GigaViewer

    (現在の「GigaViewer for Web」)」を開発しました。ユーザーが快適にマンガ作品を楽しむための各 種機能に加え、サービス提供者の運用コストを削減する管理機能、広告によるマネタイズ支援など、機 能とサービスの拡充に継続的に取り組んでいます。2021 年 11 月より、Web マンガサービス向けビュー ワに加えて、マンガアプリに対応したビューワである「GigaViewer for Apps」の提供を開始しました。 https://hatena.co.jp/press/release/entry/2022/08/04/153000 5
  6. GigaViewer の特徴 • ビューワだけではなく、web サイトのトップページや連載ページなども提供 している • 複数の web サイトを提供している(マルチテナント)

    • web だけはなく、スマートフォンアプリ用の API もある 6
  7. 「GigaViewer for Web」が採用された Web マンガサイト > 「少年ジャンプ+」「となりのヤングジャンプ」(株式会社集英社)/「マガジンポケット」「コ ミック DAYS」「&Sofa」「モーニング・ツー」(株式会社講談社)/「くらげバンチ」(株式会社新潮 社)/「コミプレ」(株式会社ヒーローズ)/「コミックボーダー」(株式会社リイド社)/「コミッ

    クガルド」(株式会社オーバーラップ)/「ゼノン編集部」(株式会社コアミックス)/ 「MAGCOMI」(株式会社マッグガーデン)/「web アクション」(株式会社双葉社)/「コミックト レイル」(株式会社芳文社)/「コミックブシロード WEB」(株式会社ブシロードクリエイティブ)/ 「FEEL web」(株式会社祥伝社)/「サンデーうぇぶり」「コロコロオンライン」(株式会社小学館) /「COMIC OGYAAA!!」(株式会社ホーム社) https://hatena.co.jp/press/release/entry/2022/08/04/153000 ※2022 年 8 月時点 7
  8. なぜ GraphQL を使いたいのか • 実装の重複を減らして効率的に開発するため アプリ用 API で GraphQL を使いたい

    • web でフロントエンドをモダンな環境に改修できるようにするため、フロントエンドとバックエン ドのインターフェイスとして GraphQL を使いたい 8
  9. GigaViewer のサーバーサイドのアプリケーションは Perl で書かれている。 Perl で GraphQL の実装ができるのか? 9

  10. できます 社内で先行事例あり(カクヨム:株式会社KADOKAWAとの共同開発) 10

  11. Perl でも GraphQL を使いたい 11

  12. どうやって Perl で GraphQL を使うのか • graphql-perl というライブラリを使う ◦ https://github.com/graphql-perl/graphql-perl

    ◦ GraphQL::Execution や GraphQL::Schema などのモジュールを提供する • graphql-perl は graphql-js を Perl に移植したもの。graphql-js は Facebook によって作 られた GraphQL のリファレンス実装 12
  13. 13 sub execute { # リクエストで渡された GraphQLのクエリ、変数、オペレーションネーム my ($class, $query,

    $variables, $operation_name) = @_; # スキーマを読み込みパースする my $schema_file = read_file('schema.graphql', binmode => ':utf8'); my $schema = GraphQL::Schema->from_doc($schema_file); # クエリをパースする my $parsed_query = GraphQL::Language::Parser::parse($query); https://gihyo.jp/dev/serial/01/perl-hackers-hub/007301 サンプルコードより スキーマ、クエリのパースはラ イブラリがやってくれる
  14. 14 # Mutation ではキャッシュしない my $cachable = !!0; for my

    $query ($parsed_query->@*) { if (($_->{operationType} // '') eq 'mutation' && ($_->{name} // '') eq $operation_name) { $cachable = !!1; } } # Contextは各 Resolver で参照できる共通のストア my $context = App::GraphQL::Context->new();
  15. my $result = GraphQL::Execution::execute( $schema, $parsed_query, # root_value {}, $context,

    $variables, $operation_name, # field resolver \&_resolver, # promise code +{ resolve => \&Promise::XS::resolved, reject => \&Promise::XS::rejected, all => \&_promise_all, }, ); # リクエストが完了したタイミングで DataLoader の遅延評価を行う Promise::XS::resolved()->then(sub { $result })->then(sub { $result = shift }); $context->dispatch_data_loaders(); return ($result, $cachable); } 15 各フィールドに対してどの値を返すの かという関数
  16. 16 GraphQLの型・フィール ドに対応するパッケージ ・サブルーチン名を動的 に組み立てて解決してい る フィールドのリゾルバをモジュールに分割して動的に解決したい 例えば `Book` 型のフィールドは`App::GraphQL::Resolver::Book`に置きたい

    sub _resolver { my ($root_value, $args, $context, $info) = @_; # $root_value: 対象のスコープの型 # $args: フィールドに渡されている引数のHashRef # $context: GraphQL::Execution::execute の第4引数に渡されたオブジェクト # $info: 型の情報やスキーマのASTなどの情報 # 解決したい対象のフィールドの名前 my $field_name = $info->{field_name}; # 単純に HashRef のフィールドであれば,その値をそのまま返す my $is_hashref = (!blessed($root_value) && ref $root_value eq 'HASH'); if ($is_hashref && exists $root_value->{$field_name}) { return $root_value->{$field_name}; } # 個別のResolverモジュールに移譲する my $parent_name = $info->{parent_type}->name; my $resolver_impl = join '::', (__PACKAGE__, 'Resolver', $parent_name); my ($resolver_loaded) = try_load_class($resolver_impl); if ($resolver_loaded && $resolver_impl->can($field_name)) { return $resolver_impl->$field_name($root_value, $args, $context, $info); } return undef; # 解決できずなにも得られなかった! }
  17. GraphQL でもキャッシュを使いたい 17

  18. REST API のキャッシュ • リソースごとに異なるエンドポイント • キャッシュサーバーの Varnish でキャッシュしている GraphQL

    API のキャッシュ • GraphQL API だと、違うクエリでもエンドポイントが同じ、クエリの内容によって キャッシュできるかどうかが異なる • そこで Perl のアプリケーション側でキャッシュ可能か判断して、Redis にクエリに対する 結果を保存している 18
  19. どうやってキャッシュ可能なクエリかを判断するのか • Mutation ならばキャッシュしない • Query でもユーザーによって結果が変わるクエリはキャッシュしない 19

  20. 再掲 # Mutation ではキャッシュしない my $cachable = !!0; for my

    $query ($parsed_query->@*) { if (($_->{operationType} // '') eq 'mutation' && ($_->{name} // '') eq $operation_name) { $cachable = !!1; } } # Contextは各 Resolver で参照できる共通のストア my $context = App::GraphQL::Context->new(); context にキャッシュ可能かのフラグをもたせて、キャッシュ可能なときだけキャッシュを作成 する 20
  21. 難しいところ • 実装ミスでキャッシュしたいクエリがキャッシュできてなかったり、キャッシュしてはい けないクエリがキャッシュされるということが起こりうる 21

  22. Context のアクセサーでキャッシュ不可のフラグを立てる # ユーザーデバイスの UUID sub device_uuid { my ($self)

    = @_; # キャッシュ不可というフラグを立てる $self->disable_cache; return $self->{device_uuid}; } sub platform { my ($self) = @_; return $self->{platform}; } 22 ユーザーの情報を参照したとき にはキャッシュ不可というフラ グを立てる キャッシュ不可でない情報を参 照したときには何もしない こうすることでキャッシュ不可な クエリをうっかりキャッシュする ことを防いでいる
  23. キャッシュのキー • アプリのプラットフォームが iOS か Android かで結果を変えることができるように実装 してある • 同じキャッシュにしているとダメなので、キャッシュキーにプラットフォームを含める

    • このような情報も context に含めている 23
  24. 事前にキャッシュを作る仕組み • マンガの公開時刻に公開前のキャッシュが残ってほしくないので、特定の時間にキャッ シュがexpireするようにしている • キャッシュがexpireしたタイミングで大量のクエリが来る問題がある(Cache Stampede) • その問題を回避するためにGraphQLクエリから事前にレスポンスのキャッシュを定期的に 作っている

    24
  25. 今後の展望 • 事前にキャッシュを作る仕組みでキャッシュの切り替えタイミングのリクエストをさばく ことには成功している • とはいえどのクエリのキャッシュをつくるかという問題は難しい • 今後は Persisted Query

    に移行予定 25
  26. まとめ • はてなのマンガビューワ GigaViewer で GraphQL を採用できた • Perl でも

    GraphQL は実装できる • GraphQL では REST API とは違うキャッシュ戦略が必要で、 GigaViewer ではアプリケーションのレイヤーでキャッシュ可能かを 判別して Redis にキャッシュを保存している 26