Mastodonを読む

はじめに

タイムライン取得処理、ここからはサーバ側の処理になります。久しぶりにRubyに戻ってきました。

app/controllers/app/v1/timelines_controller.rb

前回確認したように、ホームのタイムラインを取得する際には、「/api/v1/timelines/home」にアクセスされます。対応するメソッドはTimelinesControllerのhomeです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
-
|
|
|
|
|
|
|
|
|
|
|
!
  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

Feedはmodelsの下にあります。(Feed自体は対応するデータベーステーブルがあるわけではありません) そんなに長くないので全部貼り付け。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
-
-
|
|
!
|
-
|
|
|
|
|
|
!
|
|
|
-
|
!
|
-
|
!
!
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はインメモリのデータストア、FeedManagerはapp/libにあります。zrevrangebyscoreはキーに対して指定された範囲のリストを返すようです。

で、保存してるIDリストに対応するStatusモデルを返すと。

Feedのキャッシュ

なんか引っかかります。そう、IDリストっていつの間にキャッシュされたの?ということです。よくわからないのでとりあえず「redis」でgrepしてみたところ次のコードが引っかかりました。

app/services/precompute_feed_service.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
-
-
|
|
-
-
-
|
|
!
!
!
|
|
|
-
|
!
!
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

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
-
|
|
|
|
-
|
!
!
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

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
-
|
|
|
-
|
|
-
!
|
-
!
!
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

さて、余談気味な話が長くなりましたが、話をtimelines#homeに戻して、homeメソッドの最後に「render :index」があるのでindex.rablが実行されます。

Everything is expanded.Everything is shortened.
  1
  2
 
 
collection @statuses
extends('api/v1/statuses/show')

RABLはhome#indexの時にも出てきたJSONを作るためのDSLです。あちらではやや特殊な使われ方がされていましたが、今回はStatusリストを返すのが目的なので普通のRABLの記述になっています。淡々とJSONを構築しているだけなので省略します。生成されるJSONの仕様はこちらにあります。

おわりに

今回はサーバ側で行われるタイムライン取得処理を見てきました。基本的にはデータベースから取得してJSONを返すだけですが、ホームタイムラインの場合は気づかぬうちにキャッシュが行われ、それが使われていました。Mastodonは読んでいるとこういう「気づかぬうちに行われている」というものが多いように思います。まあそれはそもそものRailsもそうか(笑)


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-05-06 (土) 17:54:55 (2773d)