mashirozx/mastodon

View on GitHub
app/workers/scheduler/follow_recommendations_scheduler.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true

class Scheduler::FollowRecommendationsScheduler
  include Sidekiq::Worker
  include Redisable

  sidekiq_options retry: 0

  # The maximum number of accounts that can be requested in one page from the
  # API is 80, and the suggestions API does not allow pagination. This number
  # leaves some room for accounts being filtered during live access
  SET_SIZE = 100

  def perform
    # Maintaining a materialized view speeds-up subsequent queries significantly
    AccountSummary.refresh
    FollowRecommendation.refresh

    fallback_recommendations = FollowRecommendation.order(rank: :desc).limit(SET_SIZE)

    Trends.available_locales.each do |locale|
      recommendations = begin
        if AccountSummary.safe.filtered.localized(locale).exists? # We can skip the work if no accounts with that language exist
          FollowRecommendation.localized(locale).order(rank: :desc).limit(SET_SIZE).map { |recommendation| [recommendation.account_id, recommendation.rank] }
        else
          []
        end
      end

      # Use language-agnostic results if there are not enough language-specific ones
      missing = SET_SIZE - recommendations.size

      if missing.positive? && fallback_recommendations.size.positive?
        max_fallback_rank = fallback_recommendations.first.rank || 0

        # Language-specific results should be above language-agnostic ones,
        # otherwise language-agnostic ones will always overshadow them
        recommendations.map! { |(account_id, rank)| [account_id, rank + max_fallback_rank] }

        added = 0

        fallback_recommendations.each do |recommendation|
          next if recommendations.any? { |(account_id, _)| account_id == recommendation.account_id }

          recommendations << [recommendation.account_id, recommendation.rank]
          added += 1

          break if added >= missing
        end
      end

      redis.multi do |multi|
        multi.del(key(locale))

        recommendations.each do |(account_id, rank)|
          multi.zadd(key(locale), rank, account_id)
        end
      end
    end
  end

  private

  def key(locale)
    "follow_recommendations:#{locale}"
  end
end