diary.sorah.jp

いいね欄を公開する

Twitter でいいね欄が全員非公開になってしまった。

わたしはブラウザの Location Bar に tw なり x. なりを入力すると …/likes な URL がいくつかならぶ程度には他人の Likes をたくさん巡回していたし、なんなら相互に Likes 見てるな…という関係もある程度には Likes タブに依存してついった〜を楽しんでいたので大変悲しいニュースだった。

自分が読んでいたアカウントの皆さんが追従するかはともかくとして、一旦自分の Likes を公開する手筈を考えて https://like.sorah.jp/ としてデプロイした。 https://github.com/sorah/publikes にコードを置いてあるので、お手元の AWS アカウンへ terraform apply をすればみなさんも追従できます。

いいねしたツイートを集める

類似事例では Twitter API に 100 USD/mo 支払っていそう なところ、さすがにこの方向性になっちゃったサービスにお金はらいたくないので (これまでは払ってもいいと思っていましたが… クリエイターやストリーマーが全員せーのでやめてくれたらやめれる気がしますね)、それ以外での自分の Like 手段を考えることに。

はじめに Web ブラウザで自分のいいねタブをくるくるスクロールしたら中身を吸い出してアップロードしてもいいなと思ったけれど、Chrome の Extension では webRequest API があるけどこれは payload が取れず、かといって Dev Tools API は面倒くさそうで、そして HAR ファイルにも application/json の response body は含まれないのでいずれも没に…。

そんな中 @miyagawa から IFTTT Pro では取れる と教えてもらい、IFTTT からの Webhook で集めることにした。試してみたところ欠損もとくになさそうでこれをソースとすることに。

ストレージ

Webhook は Lambda Function URL で受け取り、SQS で別の Lambda Function に処理を任せて DLQ を作れるようにしている。

いつも通りストレージおよびフロントエンドに配信するデータは Amazon S3 に全部置くという前提だけど、Webhook だと 1 ツイートずつ全部ばらばらに届くという点で相性が悪い。効率を考えると 1 ページ 1 オブジェクトである程度アイテムがまとまっていた方が嬉しいけど、Webhook 単位で S3 上のページを更新するのは race condition が生まれてしまう。なお、DynamoDB は? というとあれも Recency で上から順番に pagination しながらめくっていくような用途は不得意。

思考の末、データ構造上ページを 2 層構造にした。 batch の下に pages を持たせ、batch には pages リストがあり batch 同士が一方向連結リストになっている。Webhook から挿入されるデータは 1 ツイートを 1 page として先頭の batch に挿入していくことにした。挿入自体は先に書いたように平行した Webhook リクエストがあると消失する可能性があるが、page を先に PutObject しておけばデータロスは防げるし、page のリストとなっている batch についても更新は楽観的ロックでリトライをかけることでごまかしている。batch の概念が必要なのは、 S3 のオブジェクトのリスト操作は重く、またリクエスト単価が高いためフロントエンドから ListObjectsV2 などを実行するのは現実的ではないから。また、管理が面倒なのでフロントンドからのリクエストを処理する Lambda Function なども持たせたくなかった。

もちろん、そのままだとフロントエンドでファーストビューとなる先頭の batch は page が細分化されすぎていて効率が悪いため、実際には page にはリクエストが飛ばさず key から tweet ID を抽出してリクエストせずに同じ page をブラウザ側で作成するようにしている。その上で、ある程度貯まったら裏で merge 処理として、細分化された page をある程度の単位にまとめて PutObject しなおす実装を入れた。これを範囲を区切って安全に実施するため 2 層構造にしたといってよい。

merge を行うのは細かいオブジェクトがそもそも S3 に多いとそれはそれで管理がしんどいし S3 IA などのしたときのオーバーヘッドや、バルクで何かするときに効率が落ちて面倒くさいから。そして、万が一 batch に含まれていない page があっても、batch に対応する S3 prefix に ListObjectsV2 をかけているので、merge 処理で eventual に見た目上のデータロスは補完されるようになっている。

先に書いたようにフロントエンドでの最適化処理をするくらいなら、batch に page 相当のオブジェクトを埋め込めるようにしてもよかった気はする (その上で重複する形で page 単体のオブジェクトの作成はどのみち必要) けど、まあもう動いているのでいいやという状態。

Outro

なにはともあれ、多少どころかだいぶオーバーエンジニアリングな気はしつつも、安価にいいね欄を公開できた上でみんなも追従できるような Terraform module が作れたので良かったです。

どうぞご利用ください。みんなが真似してくれたらうれしいな〜。

Published at