[[Mastodonを読む]] #contents *はじめに タイムライン取得処理、ここからはサーバ側の処理になります。久しぶりにRubyに戻ってきました。 *app/controllers/app/v1/timelines_controller.rb [#l8bc8780] 前回確認したように、ホームのタイムラインを取得する際には、「/api/v1/timelines/home」にアクセスされます。対応するメソッドはTimelinesControllerのhomeです。 #code(Ruby){{ def home @statuses = Feed.new(:home, current_account).get(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) @statuses = cache_collection(@statuses) set_maps(@statuses) next_path = api_v1_home_timeline_url(pagination_params(max_id: @statuses.last.id)) unless @statuses.empty? prev_path = api_v1_home_timeline_url(pagination_params(since_id: @statuses.first.id)) unless @statuses.empty? set_pagination_headers(next_path, prev_path) render :index end }} *app/models/feed.rb [#x2036616] Feedはmodelsの下にあります。(Feed自体は対応するデータベーステーブルがあるわけではありません) そんなに長くないので全部貼り付け。 #code(Ruby){{ class Feed def initialize(type, account) @type = type @account = account end def get(limit, max_id = nil, since_id = nil) max_id = '+inf' if max_id.blank? since_id = '-inf' if since_id.blank? unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:last).map(&:to_i) status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h unhydrated.map { |id| status_map[id] }.compact end private def key FeedManager.instance.key(@type, @account.id) end def redis Redis.current end end }} [[Redis>https://redis.io/]]はインメモリのデータストア、FeedManagerはapp/libにあります。[[zrevrangebyscore>https://redis.io/commands/zrevrangebyscore]]はキーに対して指定された範囲のリストを返すようです。 で、保存してるIDリストに対応するStatusモデルを返すと。 **Feedのキャッシュ [#m3a67f78] なんか引っかかります。そう、IDリストっていつの間にキャッシュされたの?ということです。よくわからないのでとりあえず「redis」でgrepしてみたところ次のコードが引っかかりました。 app/services/precompute_feed_service.rb #code(Ruby){{ class PrecomputeFeedService < BaseService # Fill up a user's home/mentions feed from DB and return a subset # @param [Symbol] type :home or :mentions # @param [Account] account def call(_, account) redis.pipelined do Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS / 4).each do |status| next if status.direct_visibility? || FeedManager.instance.filter?(:home, status, account.id) redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) end end end private def redis Redis.current end end }} うむ、こいつがキャッシュしてるっぽい。 キャッシュしてるところが見つかったので、今度は、じゃあ誰がPrecomputeFeedService呼んでるの?ということになります。またまたgrepのお力を借りると今度は次のコードが引っかかりました。 app/workers/regeneration_worker.rb #code(Ruby){{ class RegenerationWorker include Sidekiq::Worker sidekiq_options queue: 'pull', backtrace: true, unique: :until_executed def perform(account_id, _ = :home) PrecomputeFeedService.new.call(:home, Account.find(account_id)) end end }} でこいつは(ry、とさらにgrepしたところ、 app/controllers/application_controller.rb #code(Ruby){{ class ApplicationController < ActionController::Base before_action :set_user_activity def set_user_activity return unless !current_user.nil? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < 24.hours.ago) # Mark user as signed-in today current_user.update_tracked_fields(request) # If the sign in is after a two week break, we need to regenerate their feed RegenerationWorker.perform_async(current_user.account_id) if current_user.last_sign_in_at < 14.days.ago end }} これ以上は追いかけませんが、まあ動いているから動くのでしょう。 *app/views/app/v1/timelines/index.rabl [#z89735ab] さて、余談気味な話が長くなりましたが、話をtimelines#homeに戻して、homeメソッドの最後に「render :index」があるのでindex.rablが実行されます。 #code(Ruby){{ collection @statuses extends('api/v1/statuses/show') }} RABLはhome#indexの時にも出てきたJSONを作るためのDSLです。あちらではやや特殊な使われ方がされていましたが、今回はStatusリストを返すのが目的なので普通のRABLの記述になっています。淡々とJSONを構築しているだけなので省略します。生成されるJSONの仕様は[[こちら>https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md#status]]にあります。 *おわりに [#k857f785] 今回はサーバ側で行われるタイムライン取得処理を見てきました。基本的にはデータベースから取得してJSONを返すだけですが、ホームタイムラインの場合は気づかぬうちにキャッシュが行われ、それが使われていました。Mastodonは読んでいるとこういう「気づかぬうちに行われている」というものが多いように思います。まあそれはそもそものRailsもそうか(笑)