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

    View Slide

  2. 自己紹介
    ● はてなのマンガメディア開発チーム所属のアプリケーションエンジニ

    ● コミック DAYS アプリなどスマートフォンアプリの開発を経て、現在
    は GigaViewer のサーバーサイドの開発をしている
    2

    View Slide

  3. アジェンダ
    ● GigaViewer でも GraphQL を使いたい
    ● Perl でも GraphQL を使いたい
    ● GraphQL でもキャッシュを使いたい
    3

    View Slide

  4. GigaViewer でも GraphQL を使いたい
    4

    View Slide

  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

    View Slide

  6. GigaViewer の特徴
    ● ビューワだけではなく、web サイトのトップページや連載ページなども提供
    している
    ● 複数の web サイトを提供している(マルチテナント)
    ● web だけはなく、スマートフォンアプリ用の API もある
    6

    View Slide

  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

    View Slide

  8. なぜ GraphQL を使いたいのか
    ● 実装の重複を減らして効率的に開発するため アプリ用 API で GraphQL を使いたい
    ● web でフロントエンドをモダンな環境に改修できるようにするため、フロントエンドとバックエン
    ドのインターフェイスとして GraphQL を使いたい
    8

    View Slide

  9. GigaViewer のサーバーサイドのアプリケーションは Perl で書かれている。
    Perl で GraphQL の実装ができるのか?
    9

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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 サンプルコードより
    スキーマ、クエリのパースはラ
    イブラリがやってくれる

    View Slide

  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();

    View Slide

  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
    各フィールドに対してどの値を返すの
    かという関数

    View Slide

  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; # 解決できずなにも得られなかった!
    }

    View Slide

  17. GraphQL でもキャッシュを使いたい
    17

    View Slide

  18. REST API のキャッシュ
    ● リソースごとに異なるエンドポイント
    ● キャッシュサーバーの Varnish でキャッシュしている
    GraphQL API のキャッシュ
    ● GraphQL API だと、違うクエリでもエンドポイントが同じ、クエリの内容によって
    キャッシュできるかどうかが異なる
    ● そこで Perl のアプリケーション側でキャッシュ可能か判断して、Redis にクエリに対する
    結果を保存している
    18

    View Slide

  19. どうやってキャッシュ可能なクエリかを判断するのか
    ● Mutation ならばキャッシュしない
    ● Query でもユーザーによって結果が変わるクエリはキャッシュしない
    19

    View Slide

  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

    View Slide

  21. 難しいところ
    ● 実装ミスでキャッシュしたいクエリがキャッシュできてなかったり、キャッシュしてはい
    けないクエリがキャッシュされるということが起こりうる
    21

    View Slide

  22. Context のアクセサーでキャッシュ不可のフラグを立てる
    # ユーザーデバイスの UUID
    sub device_uuid {
    my ($self) = @_;
    # キャッシュ不可というフラグを立てる
    $self->disable_cache;
    return $self->{device_uuid};
    }
    sub platform {
    my ($self) = @_;
    return $self->{platform};
    }
    22
    ユーザーの情報を参照したとき
    にはキャッシュ不可というフラ
    グを立てる
    キャッシュ不可でない情報を参
    照したときには何もしない
    こうすることでキャッシュ不可な
    クエリをうっかりキャッシュする
    ことを防いでいる

    View Slide

  23. キャッシュのキー
    ● アプリのプラットフォームが iOS か Android かで結果を変えることができるように実装
    してある
    ● 同じキャッシュにしているとダメなので、キャッシュキーにプラットフォームを含める
    ● このような情報も context に含めている
    23

    View Slide

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

    View Slide

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

    View Slide

  26. まとめ
    ● はてなのマンガビューワ GigaViewer で GraphQL を採用できた
    ● Perl でも GraphQL は実装できる
    ● GraphQL では REST API とは違うキャッシュ戦略が必要で、
    GigaViewer ではアプリケーションのレイヤーでキャッシュ可能かを
    判別して Redis にキャッシュを保存している
    26

    View Slide