くりにっき

フルスタックキュアエンジニアです

細かすぎて伝わりづらいプリマップの改修ポイント #プリッカソン

これは #プリッカソン Advent Calendar 2020 - Adventar の2日目です。

10月の プリマップ 公開以降見た目はほとんど変わってないのですが裏側をちょいちょい改善してるのでメモ

sue445.hatenablog.com

ジオコーディング前に住所をいい感じにしている

公式の店舗リストの住所には「東京都新宿区西新宿1-11-1ゲームホビー館」のように建物名が入ってることが多いのですが、この手の住所をそのままジオコーディング(住所から緯度経度を取得)すると高確率で意図しない緯度経度が返ってくるため「東京都新宿区西新宿1-11-1」のように建物名を削ってやる必要があります。

この手の前処理はリリース前から実装自体はしてたものの、日本の住所表記はバリエーションが豊富なのでリリース以降も気づきベースでちょいちょい修正してました

プロダクトコードだけ見ると正規表現がアレなのでテストコード見た方が分かりやすいと思います。

データ集計処理の高速化

2200件近い店舗情報をFirestoreに一度に保存する関係上GCPのPub/Subのtopic *1 を使ってCloud Functionsで大量に並列処理をしているのですが、topicに積むところが遅かったのでパフォーマンスチューニングしました。

デバッグログ仕込んで各処理の実行時間を計測したら改善できそうな処理が2箇所あったのでそれぞれ修正。

github.com

2200件もあるとPub/Subのtopicに積むだけで遅いのでgoroutineで並列化しました。ここだけで50秒から1~7秒くらいに改善。(実行時間があまり安定しない・・・)

github.com

削除された店舗一覧を調べるために「Firestoreに保存済の店舗一覧 - prismdbで取得した店舗一覧」みたいなことをやるためにGoのslice同士の引き算を行ってるのですが、そこの処理が遅かったのを改修。 ベンチマークレベルだと300倍速くなっていてCloud Functions上でも9秒から1秒前後に改善。

改修前はトータルで1分弱かかってた処理が4~8秒で終わるようになりました。

公式のデータが間違っていたのでタカラトミーアーツさんのサポートに報告して直してもらった

プリマップでは間接的*2に公式の店舗リストを使ってるのですが公式のデータが一部間違ってるせいでプリマップのデータも変なことになってたのでタカラトミーアーツさんに報告して直してもらいました。

件数多かったけど報告したその日のうちに全部直してもらったので不満を持ってる人はどんどん報告すればいいと思います。

余談ですがタカラトミーアーツサポートへの連絡先は2つありました。

faq.takaratomy-arts.co.jp

www.takaratomy-arts.co.jp

1つ目は返信不要なやつで2つ目は返信を受け取りたい時のものかと思われます。僕は修正報告を受けたかったので後者で報告しました。

プリパラとプリ☆チャンで店舗名が微妙に食い違ってるんで名寄せした

github.com

プリマップでは店舗名をFirestoreのコレクションのKeyにしてるのでKeyが食い違うと同一店舗なのにプリパラとプリ☆チャンで1つずつ出てくる状態になって不便です。(こういうのが2200件中50件くらいありました)

明らかに情報が間違ってる場合には公式に直してもらえばいいんですが、今回のケースは完全にこっちの都合なのでプリマップ側で名寄せしてます。

ほとんどの場合「△△△△△(ゲームセンターや量販店の名前) ○○(支店名)店」と「△△△△△ ○○」で表記ゆれしてたので機械的に「店」を削るだけで済んだのですが*3、それでも名寄せできなかったものがあったのでそこだけ名寄せ用のマッピングを作りました *4

今更だけどアイカツ!16話のチケット5万枚を2秒で売り切るシステムについて本気で考察してみた

発端

普通の人は5万枚のチケットを一瞬でさばいた神崎美月さん*1知名度に着目しがちですが、僕は職業柄システムの方が気になりました。

お前誰よ?

会社でインフラを見ています。業務でのインフラ歴は4〜5年くらい。社内だとAWSGCP両方分かるマンとして認知されています。

前置き

ネタなので過度なツッコミは禁止

構成

webサーバ

チケット5万枚を2秒で完売させるためには雑な見積もりでサーバ数百台必要だと仮定します。

これだけの規模をオンプレミスで調達するのは大変なのでAWSGCPなどのクラウドを使うことになると思います。

オートスケール(アクセス数やサーバの負荷に応じて自動でサーバを増減させる)という手もあるのですが、2秒で数百台までスケールさせるのはさすがに厳しいと思います。

参考:自社で運用してるシステムでGKEのオートスケールを導入しているのですがリクエスト数急増でサーバ5台増やすのに10分くらいかかっています。*2

チケット販売開始の日時はあらかじめ決まっているからその日時に合わせてあらかじめサーバをその台数まで増やして待機させておくのがベターでしょう。

これはAWSGCPのどちらにも言えることですが、1つのAWSアカウントやGCPプロジェクトで作れるサーバの台数には上限があり数百台というのは初期状態では作れないのでサポートに問い合わせて上限緩和申請をすることになります。クラウド事業者や希望台数にもよってサポートでの対応完了日数が変わるのでなんとも言えないけど僕ならバッファ込で2〜3営業日くらいは見込みます。

データベース

チケットの在庫やチケットを販売したという情報を持つためにデータベースは必須です。(以降、データベース = RDBを前提に書きます。FirestoreやDynamoDBのようなKVSは知らん)

webサーバは横に並べることで負荷分散できるのですがデータベースは簡単に増やせないしオートスケールもできないのでここがボトルネックになりがちです。

AWSのRDS, AuroraやGCPのCloud SQLのようなフルマネージドなデータベースを使えばスケールアップ・スケールダウンはできるのですが、あくまでも手動だしスペック変更時に基本的に数分間のダウンタイムが発生するのでこれも予めつよつよスペックにしとく必要があります。

僕なら迷わず一番高いスペックを選びます。

決済システム

今日日自前で決済システムを持つことはあまりなく、決済専用の外部サービスを使うことが多いです。

自分のアプリケーション(チケット販売サイト)が秒間2.5万リクエストに耐えられてもその先の決済システムが耐えられないと思います。(2秒間に5万リクエストとかなんて下手なDDoSより量が多いし普通にネットワークで詰まるのでは...)

負荷試験

神崎美月さんといえばアイカツ界の超メジャーアイドル。そのライブチケットとなれば当然負荷も予測されるので負荷試験も必要でしょう。

負荷試験をしないと実際にどれだけのサーバ台数が必要なのか分からないので僕ならやります。

当日の監視

未曾有のアクセスが来るのが分かってるのであれば当日立ち会っての監視も当然やるでしょう。僕なら絶対やります

まとめ

余談

*1:https://dic.pixiv.net/a/%E7%A5%9E%E5%B4%8E%E7%BE%8E%E6%9C%88

*2:GKEだとpodをオートスケールする Horizontal Pod Autoscaler(通称HPA)とpodを収容してるnode(≒サーバ)をオートスケールするクラスタオートスケーラーの2種類のオートスケールがあるのですがここで言ってるのは後者の方です

Goで複数パッケージ構成のプロジェクトでもちゃんとカバレッジ集計する(GitLab編)

tl;dr;

gcov + lcovが無難

Goで複数パッケージ構成の場合何が問題か?

Goでテストを実行する時に -cover をつけるとお手軽にカバレッジが集計できるのですが、1パッケージずつしか出力されないので複数パッケージ構成のプロジェクトの時にプロジェクト全体のカバレッジが集計できずに困ります。*1

$ go test -count=1 -cover ./...

ok      gitlab.com/sue445/tanuki_reminder   0.066s  coverage: 77.2% of statements
ok      gitlab.com/sue445/tanuki_reminder/enum  0.026s  coverage: 100.0% of statements

GitHubの場合

songmu.jp

にもあるように

  1. -coverprofile coverage.out でgcov形式で出力
  2. jandelgado/gcov2lcov-action でlcov形式に変換
  3. lcovファイルをCoverallsに送信

が今の所最適解だと思います。

GitLabの場合

GitHubと違いGitLabはカバレッジ集計に標準対応しているというのがあります。

https://docs.gitlab.com/ee/ci/pipelines/settings.html#test-coverage-parsing

CIのログに出力されたテストカバレッジっぽい文字列を正規表現で抽出するという割と力技な仕組なのですが、各言語のデファクトツールに関してはだいたい正規表現が用意されています。

f:id:sue445:20201108170855p:plain

GitLabのリポジトリ上で正規表現を設定しておくだけで最終的にリポジトリ上でカバレッジの推移のグラフが見れます。 *2

f:id:sue445:20201108171107p:plain

Coverallsに比べればだいぶ貧弱なのですがリポジトリに標準機能でついているという強みはあります。

もしこれで困る場合はCoverallsのような他ツールを使うか*3、lcovで生成したカバレッジレポートをGitLab Pagesにアップロードすればいいと思います。

本題:GitLabで複数パッケージ構成のGoプロジェクトのカバレッジを集計したい

tanuki_reminder だと当初 /coverage: \d+.\d+% of statements/ のような正規表現カバレッジを収集してたのですが、1つのログに複数のカバレッジが出ていると後の方にあるものがそのジョブのカバレッジとして採用されて複数パッケージ構成になった時に変なことになりました。*4

f:id:sue445:20201108172019p:plain

いくつか手法を考えたのですが、lcovのgenhtmlコマンドでHTML形式のカバレッジレポートを作る時にプロジェクト全体のカバレッジがログに出力されることに着目してこいつを利用するようにしました

f:id:sue445:20201108173154p:plain

.gitlab-ci.ymlのサンプル

test:
  image: golang:1.15

  stage: test

  variables:
    GCOV2LCOV_VERSION: v1.0.4

  script:
    - mkdir -p coverage/
    - go test -coverprofile coverage/coverage.out -covermode atomic ./...

    # Install and run gcov2lcov
    - wget https://github.com/jandelgado/gcov2lcov/releases/download/${GCOV2LCOV_VERSION}/gcov2lcov-linux-amd64.tar.gz -q -O - | tar xvzf - --strip 1
    - chmod 755 gcov2lcov-linux-amd64
    - export GOROOT=$(go env GOROOT)
    - ./gcov2lcov-linux-amd64 -infile "coverage/coverage.out" -outfile "coverage/coverage.lcov"

    # Install and run lcov
    - apt-get update
    - apt-get install -y lcov
    - genhtml coverage/coverage.lcov -o coverage/

  # regular expression for genhtml
  coverage: '/lines.*\d+\.\d+%/'

  artifacts:
    paths:
      - coverage/

lcovを使ってるので後半部分はGo以外でも流用できそう

compact_blank gemのメンテを終了した

Rails 6.1.0.rc1がリリースされましたが*1activesupport本体に #compact_blank#compact_blank! のメソッドが入った*2ので6.1.0.rc1以降と一緒に入れられないようにしてメンテ終了宣言しました。

github.com

rubocop v1系 + rubocop-rspec v1系でエラーになる件の回避策

tl;dr;

どっちのケースの場合もrubocop-rspec v2.0.0.preで直ってるので Gemfile に下記のように書けば直る

group :development do
  gem "rubocop-rspec", ">= 2.0.0.pre", require: false
end

ケース1: rubocop-rspec内でエラーになる

エラー内容

-dデバッグ出力)を有効にしないとエラー内容が表示されないので注意

$ bundle exec rubocop -d

(中略)

An error occurred while RSpec/FactoryBot/CreateList cop was inspecting /Users/sue445/workspace/github.com/sue445/app-stat-api/spec/app_spec.rb:1:0.
undefined method `to_sym' for nil:NilClass
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/mixin/configurable_enforced_style.rb:69:in `style'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-rspec-1.42.0/lib/rubocop/cop/rspec/factory_bot/create_list.rb:51:in `on_block'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:100:in `public_send'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:100:in `block (2 levels) in trigger_responding_cops'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:166:in `with_cop_error_handling'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:99:in `block in trigger_responding_cops'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:98:in `each'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:98:in `trigger_responding_cops'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:69:in `on_block'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-ast-1.1.0/lib/rubocop/ast/traversal.rb:20:in `walk'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/commissioner.rb:86:in `investigate'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/team.rb:157:in `investigate_partial'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cop/team.rb:83:in `investigate'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:315:in `inspect_file'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:259:in `block in do_inspection_loop'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:293:in `block in iterate_until_no_changes'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:286:in `loop'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:286:in `iterate_until_no_changes'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:255:in `do_inspection_loop'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:132:in `block in file_offenses'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:157:in `file_offense_cache'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:131:in `file_offenses'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:122:in `process_file'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:101:in `block in each_inspected_file'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:100:in `each'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:100:in `reduce'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:100:in `each_inspected_file'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:86:in `inspect_files'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/runner.rb:47:in `run'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli/command/execute_runner.rb:25:in `execute_runner'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli/command/execute_runner.rb:17:in `run'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli/command.rb:11:in `run'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli/environment.rb:18:in `run'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli.rb:65:in `run_command'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli.rb:72:in `execute_runners'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/lib/rubocop/cli.rb:41:in `run'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/exe/rubocop:13:in `block in <top (required)>'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/benchmark.rb:308:in `realtime'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/gems/rubocop-1.1.0/exe/rubocop:12:in `<top (required)>'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/bin/rubocop:23:in `load'
/Users/sue445/workspace/github.com/sue445/app-stat-api/vendor/bundle/ruby/2.7.0/bin/rubocop:23:in `<top (required)>'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/cli/exec.rb:63:in `load'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/cli/exec.rb:63:in `kernel_load'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/cli/exec.rb:28:in `run'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/cli.rb:476:in `exec'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/vendor/thor/lib/thor.rb:399:in `dispatch'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/cli.rb:30:in `dispatch'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/vendor/thor/lib/thor/base.rb:476:in `start'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/cli.rb:24:in `start'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bundler-2.1.4/libexec/bundle:46:in `block in <top (required)>'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/2.7.0/bundler/friendly_errors.rb:123:in `with_friendly_errors'
/Users/sue445/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bundler-2.1.4/libexec/bundle:34:in `<top (required)>'
/Users/sue445/.rbenv/versions/2.7.2/bin/bundle:23:in `load'
/Users/sue445/.rbenv/versions/2.7.2/bin/bundle:23:in `<main>'

関連するissue

github.com

実際に対応したコミット

https://github.com/sue445/app-stat-api/pull/631/commits/7013ba612d2f1f663e2b8e60816655de6770602e

ケース2: 意図せずrubocop-rspecのバージョンが下がる

https://github.com/sue445/twittodon/pull/441/commits/3abc21db31db2532283af5ef790ea32e62a804cb#diff-89cade48462044ee1b672dc5f4c3ec250fbd29effcd8932096a23c1283c6731fR102

注意点

https://github.com/rubocop-hq/rubocop-rspec/blob/v2.0.0.pre/CHANGELOG.md#200pre-2020-10-22

v2で 下記のようなbreaking changeがあるので注意

(Potentially breaking) Change namespace of several cops (Capybara/* -> RSpec/Capybara/*, FactoryBot/* -> RSpec/FactoryBot/*, Rails/* -> RSpec/Rails/*).

プリマップというプリパラやプリ☆チャンが遊べるお店を探せる地図を作った

タイトルが全て

primap.web.app

f:id:sue445:20201013232154p:plain

モチベーション

以前 プリ☆チャンMAP を作ったことがあるのですが、下記のような不満がありました。

  • プリ☆チャンの公式店舗一覧スクレイピングしてスプレッドシートに入れるところまでは自動化できたが、そこからGoogleマイマップに取り込む作業が手作業
  • スプレッドシートに住所カラムを作っておけばGoogleマイマップがいい感じに緯度経度に変換してくれるのだが、公式の住所のフォーマットがおかしいと稀に緯度経度が取れないので手動で補正する必要がある
  • 公式のショップ一覧は定期的に更新されるのでそれに追従するのが大変
  • プリ☆チャンMAP作成後にプリパラオルフレンズの稼働が始まったのだがそれの地図を作るのも大変

そのため完全に作り直しました。

使い方

https://primap.web.app/ を開くといい感じに現在地周辺のプリ☆チャンやプリパラで遊べる店舗の一覧が出ます。

f:id:sue445:20201013233308p:plain

技術的なこと

リポジトリ

github.com

大雑把な仕組み

  1. PrismDB から店舗一覧を取得
  2. Geocoding APIで店舗の住所から緯度経度を取得してFirestoreに保存
  3. Firestoreに保存されている店舗をいい感じに地図上に表示

開発期間はトータルで1ヶ月くらいです

サーバサイド

俗に言うサーバサイドは最初はあったけど途中で捨てました

当初の構成だとCloud RunでGoで書いたAPIやジョブをホスティングする予定だったんですが、APIはFirebaseでいいことに気づいたので途中で方針転換して、その結果サーバサイドがなくなりました。(サーバレス)

ジョブ

採用技術は下記

  • Go
  • Cloud Functions
  • Cloud Scheduler
  • Cloud Pub/Sub
  • Secret Manager
  • Geocoding API
  • Serverless Framework

店舗が数千件あって1つのファンクションの中で全ての緯度経度の取得をするのは厳しいと判断して、PrismDBで取得した店舗の件数分だけPub/Subでqueueに積んで非同期で処理するようにしました

Geocoding APIを一度にたくさん叩きすぎるとReteLimitに引っかかるのでリトライも必要になるのですが、Pub/Subで店舗1件ずつ処理することでその辺のリトライを何も考えなくてよかったのは楽でした。(Pub/Subで呼んだファンクションがエラーになれば勝手にリトライされるので)

フロントエンド

採用技術は下記

  • TypeScript
  • React
    • Vue.js + TSは過去にやったことあるので今回は今まで使ったことのないReactを採用
    • React歴3週間くらい
  • Tailwind CSS
    • 社内でちょいちょい名前を聞いていたので採用
  • Firebase Hosting
  • Google Maps

その他使ったもの

  • GitHub Actions
  • Sentry
  • Terraform

あたり

こだわりポイント

Cloud RunからCloud Functionsに移行して変わったこと

メリット

  • デプロイフローがシンプルになった
    • Cloud Runでアプリを動かすためにはDockerイメージが必要なので必然的にCIでもdocker buildしてContainer Registryにpushする必要があるのだが(GitHub Actions上で1分弱)、Cloud Functions化することでそのフローが完全に不要になった
  • Cloud RunでCloud SchedulerやPub/Subのリクエストを受けるためにはhttpのエンドポイントが必要だったが、Cloud Functions化することでhttp周りの実装が不要になった
    • Cloud Functions側にPub/Sub専用のエンドポイントがある

デメリット

  • Cloud Run時代はデプロイが2分弱だったが*1、Cloud Functions移行後にデプロイに5〜6分かかるようになった
    • Serverless Frameworkのバックエンドで使ってるDeployment Managerの様子を見る感じ純粋にCloud Functionsへのデプロイが一番時間がかかってるのだが、具体的にどこで時間がかかってるのかが特定できないので謎。(おそらくgo getかビルドのどちらかだと思うのだが...)
    • 1つのバイナリに2つのファンクションが含まれているのでそれらを分割すれば早くなる可能性はありそう
  • Cloud Functionsだと最新のGoが1.13
    • 早く1.15使わせてほしい...

GoのstructからTypeScriptのclassを自動生成するようにした

FirestoreにはGoのstructで保存してるのですが、全く同じデータをフロントエンドで使うために tscriptify でTypeScriptのclassに変換しています。

Firestoreに緯度経度の型はあるのだが、実際にはそれだけだと検索で使えなくてハマった

現在地の緯度経度からFirestoreで検索しようとしたのですが当初うまくいきませんでした。(明らかに現在地から外れた店舗が検索にヒットする)

調べると https://stackoverflow.com/questions/46630507/how-to-run-a-geo-nearby-query-with-firestore

Firestore does not support actual GeoPoint queries at present so while the below query executes successfully, it only filters by latitude, not by longitude and thus will return many results that are not nearby.

とあるように、Firestoreは実際のGeoPointクエリをサポートしていないため、latlng型で検索しても緯度しか考慮されないということでした。

そのため緯度経度から算出したGeoHashも別途Firestoreに保存することでようやく現在地周辺の検索ができるようになりました。

ディレクトリ構成を色々変えた

開発中に何回かディレクトリ構成を作り直しました

  • 1段階目
    • 最初はオーソドックスなGoのアプリのディレクトリ構成
  • 2段階目
    • frontend実装直後
    • GoはそのままでTSとReact関係のソースだけfrontendに入れた形
  • 3段階目
    • serverとfrontendの2つのディレクトリ構成
    • この時点ではmakeやnpmのコマンド実行時にいちいちcdするのが嫌だという気持ちがあってこの構成でした
    • TSやReact関係のソースは全部frontendに格納できたのですが、Goはビルドの関係でトップに一部ソースが残ってます
  • 4段階目(今)
    • 「makeやnpmのコマンド実行時にいちいちcdするのが嫌だ」という気持ちはあったんですが、それ以上にトップの見通しが悪いのが嫌だったので完全にディレクトリを分けました(手の平クルー)

f:id:sue445:20201014000907p:plain

frontendとfunctionの接点がfirestoreだけだったのでここまで振り切れたかもしれないです。

本当ならfrontendとfunctionでリポジトリを分けてもよかったのですが、前述の通りGoのstructからTypeScriptのclassを生成してるためリポジトリを分けると逆に連携が難しくなるためモノリシックリポジトリにしました。

port 55301

せっかくなので(?)devServerは55301番ポートを使うようにしました。

副産物

プリマップでSecretManagerを使ってるのですがラッパとして途中でgcp-secretmanagerenvができました

sue445.hatenablog.com

*1:docker buildに1分半でgcloud run deployが30秒

pixela gem v2.0.0をリリースした

pixela v1.19.0 で機能削除が行われたのでgemのメジャーバージョンを上げました。

blog.a-know.me

Breaking changeついでに他にも非互換の変更を入れています

変更内容と変更意図

https://github.com/sue445/pixela/blob/master/CHANGELOG.md

  • faraday v0.x系のサポート終了
  • Ruby 2.3のサポート終了
    • https://github.com/sue445/pixela/pull/78
    • faraday v1準拠で最低バージョンは2.3のままでいこうかなと思ったのですが、Ruby 2.3をサポートしたままだと Hash#compact が使えなくて微妙な感じなので最低バージョンを2.4に引き上げました