end
def subscribeable_accounts
- Account.with_followers.remote.where(domain: params[:by_domain])
+ Account.remote.where(protocol: :ostatus).where(domain: params[:by_domain])
end
def filter_params
end
def default_accounts
- Account.includes(:active_relationships).references(:active_relationships)
+ Account.includes(:active_relationships, :account_stat).references(:active_relationships)
end
def paginated_follows
end
def default_accounts
- Account.includes(:passive_relationships).references(:passive_relationships)
+ Account.includes(:passive_relationships, :account_stat).references(:passive_relationships)
end
def paginated_follows
end
def paginated_blocks
- @paginated_blocks ||= Block.eager_load(:target_account)
+ @paginated_blocks ||= Block.eager_load(target_account: :account_stat)
.where(account: current_account)
.paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
end
def endorsed_accounts
- current_account.endorsed_accounts
+ current_account.endorsed_accounts.includes(:account_stat)
end
def insert_pagination_headers
end
def default_accounts
- Account.includes(:follow_requests).references(:follow_requests)
+ Account.includes(:follow_requests, :account_stat).references(:follow_requests)
end
def paginated_follow_requests
def load_accounts
if unlimited?
- @list.accounts.all
+ @list.accounts.includes(:account_stat).all
else
- @list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
+ @list.accounts.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
end
end
def default_accounts
Account
- .includes(:favourites)
+ .includes(:favourites, :account_stat)
.references(:favourites)
.where(favourites: { status_id: @status.id })
end
end
def default_accounts
- Account.includes(:statuses).references(:statuses)
+ Account.includes(:statuses, :account_stat).references(:statuses)
end
def paginated_statuses
# suspended :boolean default(FALSE), not null
# locked :boolean default(FALSE), not null
# header_remote_url :string default(""), not null
-# statuses_count :integer default(0), not null
-# followers_count :integer default(0), not null
-# following_count :integer default(0), not null
# last_webfingered_at :datetime
# inbox_url :string default(""), not null
# outbox_url :string default(""), not null
include AccountInteractions
include Attachmentable
include Paginable
+ include AccountCounters
enum protocol: [:ostatus, :activitypub]
scope :remote, -> { where.not(domain: nil) }
scope :local, -> { where(domain: nil) }
- scope :without_followers, -> { where(followers_count: 0) }
- scope :with_followers, -> { where('followers_count > 0') }
scope :expiring, ->(time) { remote.where.not(subscription_expires_at: nil).where('subscription_expires_at < ?', time) }
scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) }
scope :silenced, -> { where(silenced: true) }
LIMIT ?
SQL
- find_by_sql([sql, limit])
+ records = find_by_sql([sql, limit])
+ ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
+ records
end
def advanced_search_for(terms, account, limit = 10, following = false)
LIMIT ?
SQL
- find_by_sql([sql, account.id, account.id, account.id, limit])
+ records = find_by_sql([sql, account.id, account.id, account.id, limit])
else
sql = <<-SQL.squish
SELECT
LIMIT ?
SQL
- find_by_sql([sql, account.id, account.id, limit])
+ records = find_by_sql([sql, account.id, account.id, limit])
end
+
+ ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
+ records
end
private
--- /dev/null
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: account_stats
+#
+# id :bigint(8) not null, primary key
+# account_id :bigint(8) not null
+# statuses_count :bigint(8) default(0), not null
+# following_count :bigint(8) default(0), not null
+# followers_count :bigint(8) default(0), not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+
+class AccountStat < ApplicationRecord
+ belongs_to :account, inverse_of: :account_stat
+
+ def increment_count!(key)
+ update(key => public_send(key) + 1)
+ end
+
+ def decrement_count!(key)
+ update(key => [public_send(key) - 1, 0].max)
+ end
+end
--- /dev/null
+# frozen_string_literal: true
+
+module AccountCounters
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :account_stat, inverse_of: :account
+ after_save :save_account_stat
+ end
+
+ delegate :statuses_count,
+ :statuses_count=,
+ :following_count,
+ :following_count=,
+ :followers_count,
+ :followers_count=,
+ :increment_count!,
+ :decrement_count!,
+ to: :account_stat
+
+ def account_stat
+ super || build_account_stat
+ end
+
+ private
+
+ def save_account_stat
+ return unless account_stat&.changed?
+ account_stat.save
+ end
+end
include Paginable
include RelationshipCacheable
- belongs_to :account, counter_cache: :following_count
-
- belongs_to :target_account,
- class_name: 'Account',
- counter_cache: :followers_count
+ belongs_to :account
+ belongs_to :target_account, class_name: 'Account'
has_one :notification, as: :activity, dependent: :destroy
end
before_validation :set_uri, only: :create
+ after_create :increment_cache_counters
after_destroy :remove_endorsements
+ after_destroy :decrement_cache_counters
private
def remove_endorsements
AccountPin.where(target_account_id: target_account_id, account_id: account_id).delete_all
end
+
+ def increment_cache_counters
+ account&.increment_count!(:following_count)
+ target_account&.increment_count!(:followers_count)
+ end
+
+ def decrement_cache_counters
+ account&.decrement_count!(:following_count)
+ target_account&.decrement_count!(:followers_count)
+ end
end
return if account_ids.empty?
- accounts = Account.where(id: account_ids).each_with_object({}) { |a, h| h[a.id] = a }
+ accounts = Account.where(id: account_ids).includes(:account_stat).each_with_object({}) { |a, h| h[a.id] = a }
cached_items.each do |item|
item.from_account = accounts[item.from_account_id]
end
}
- cache_associated :account,
- :application,
+ cache_associated :application,
:media_attachments,
:conversation,
:status_stat,
:tags,
:preview_cards,
:stream_entry,
- active_mentions: :account,
+ account: :account_stat,
+ active_mentions: { account: :account_stat },
reblog: [
- :account,
:application,
:stream_entry,
:tags,
:media_attachments,
:conversation,
:status_stat,
- active_mentions: :account,
+ account: :account_stat,
+ active_mentions: { account: :account_stat },
],
- thread: :account
+ thread: { account: :account_stat }
delegate :domain, to: :account, prefix: true
return if account_ids.empty?
- accounts = Account.where(id: account_ids).each_with_object({}) { |a, h| h[a.id] = a }
+ accounts = Account.where(id: account_ids).includes(:account_stat).each_with_object({}) { |a, h| h[a.id] = a }
cached_items.each do |item|
item.account = accounts[item.account_id]
def increment_counter_caches
return if direct_visibility?
- if association(:account).loaded?
- account.update_attribute(:statuses_count, account.statuses_count + 1)
- else
- Account.where(id: account_id).update_all('statuses_count = COALESCE(statuses_count, 0) + 1')
- end
-
+ account&.increment_count!(:statuses_count)
reblog&.increment_count!(:reblogs_count) if reblog?
thread&.increment_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
end
def decrement_counter_caches
return if direct_visibility? || marked_for_mass_destruction?
- if association(:account).loaded?
- account.update_attribute(:statuses_count, [account.statuses_count - 1, 0].max)
- else
- Account.where(id: account_id).update_all('statuses_count = GREATEST(COALESCE(statuses_count, 0) - 1, 0)')
- end
-
+ account&.decrement_count!(:statuses_count)
reblog&.decrement_count!(:reblogs_count) if reblog?
thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?)
end
end
def status_count
- Rails.cache.fetch('local_status_count') { Account.local.sum(:statuses_count) }
+ Rails.cache.fetch('local_status_count') { Account.local.joins(:account_stat).sum('account_stats.statuses_count') }
end
def domain_count
--- /dev/null
+class CreateAccountStats < ActiveRecord::Migration[5.2]
+ def change
+ create_table :account_stats do |t|
+ t.belongs_to :account, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true }
+ t.bigint :statuses_count, null: false, default: 0
+ t.bigint :following_count, null: false, default: 0
+ t.bigint :followers_count, null: false, default: 0
+
+ t.timestamps
+ end
+ end
+end
--- /dev/null
+class CopyAccountStats < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+
+ def up
+ safety_assured do
+ if supports_upsert?
+ up_fast
+ else
+ up_slow
+ end
+ end
+ end
+
+ def down
+ # Nothing
+ end
+
+ private
+
+ def supports_upsert?
+ version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i
+ version >= 90500
+ end
+
+ def up_fast
+ say 'Upsert is available, importing counters using the fast method'
+
+ Account.unscoped.select('id').find_in_batches(batch_size: 5_000) do |accounts|
+ execute <<-SQL.squish
+ INSERT INTO account_stats (account_id, statuses_count, following_count, followers_count, created_at, updated_at)
+ SELECT id, statuses_count, following_count, followers_count, created_at, updated_at
+ FROM accounts
+ WHERE id IN (#{accounts.map(&:id).join(', ')})
+ ON CONFLICT (account_id) DO UPDATE
+ SET statuses_count = EXCLUDED.statuses_count, following_count = EXCLUDED.following_count, followers_count = EXCLUDED.followers_count
+ SQL
+ end
+ end
+
+ def up_slow
+ say 'Upsert is not available in PostgreSQL below 9.5, falling back to slow import of counters'
+
+ # We cannot use bulk INSERT or overarching transactions here because of possible
+ # uniqueness violations that we need to skip over
+ Account.unscoped.select('id, statuses_count, following_count, followers_count, created_at, updated_at').find_each do |account|
+ begin
+ params = [[nil, account.id], [nil, account.statuses_count], [nil, account.following_count], [nil, account.followers_count], [nil, account.created_at], [nil, account.updated_at]]
+ exec_insert('INSERT INTO account_stats (account_id, statuses_count, following_count, followers_count, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6)', nil, params)
+ rescue ActiveRecord::RecordNotUnique
+ next
+ end
+ end
+ end
+end
--- /dev/null
+# frozen_string_literal: true
+
+class CopyAccountStatsCleanup < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+
+ def change
+ safety_assured do
+ remove_column :accounts, :statuses_count, :integer, default: 0, null: false
+ remove_column :accounts, :following_count, :integer, default: 0, null: false
+ remove_column :accounts, :followers_count, :integer, default: 0, null: false
+ end
+ end
+end
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2018_10_26_034033) do
+ActiveRecord::Schema.define(version: 2018_11_16_184611) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
t.index ["target_account_id"], name: "index_account_pins_on_target_account_id"
end
+ create_table "account_stats", force: :cascade do |t|
+ t.bigint "account_id", null: false
+ t.bigint "statuses_count", default: 0, null: false
+ t.bigint "following_count", default: 0, null: false
+ t.bigint "followers_count", default: 0, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["account_id"], name: "index_account_stats_on_account_id", unique: true
+ end
+
create_table "accounts", force: :cascade do |t|
t.string "username", default: "", null: false
t.string "domain"
t.boolean "suspended", default: false, null: false
t.boolean "locked", default: false, null: false
t.string "header_remote_url", default: "", null: false
- t.integer "statuses_count", default: 0, null: false
- t.integer "followers_count", default: 0, null: false
- t.integer "following_count", default: 0, null: false
t.datetime "last_webfingered_at"
t.string "inbox_url", default: "", null: false
t.string "outbox_url", default: "", null: false
add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id"
add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade
add_foreign_key "account_pins", "accounts", on_delete: :cascade
+ add_foreign_key "account_stats", "accounts", on_delete: :cascade
add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify
add_foreign_key "admin_action_logs", "accounts", on_delete: :cascade
add_foreign_key "backups", "users", on_delete: :nullify
--- /dev/null
+Fabricator(:account_stat) do
+ account nil
+ statuses_count ""
+ following_count ""
+ followers_count ""
+end
expect(Account.suspended).to match_array([account_1])
end
end
-
- describe 'without_followers' do
- it 'returns a relation of accounts without followers' do
- account_1 = Fabricate(:account)
- account_2 = Fabricate(:account)
- Fabricate(:follow, account: account_1, target_account: account_2)
- expect(Account.without_followers).to match_array([account_1])
- end
- end
-
- describe 'with_followers' do
- it 'returns a relation of accounts with followers' do
- account_1 = Fabricate(:account)
- account_2 = Fabricate(:account)
- Fabricate(:follow, account: account_1, target_account: account_2)
- expect(Account.with_followers).to match_array([account_2])
- end
- end
end
context 'when is local' do
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe AccountStat, type: :model do
+end
before do
allow(accounts_with_ids).to receive(:[]).with(stale_account1.id).and_return(account1)
allow(accounts_with_ids).to receive(:[]).with(stale_account2.id).and_return(account2)
- allow(Account).to receive_message_chain(:where, :each_with_object).and_return(accounts_with_ids)
+ allow(Account).to receive_message_chain(:where, :includes, :each_with_object).and_return(accounts_with_ids)
end
let(:cached_items) do
require 'rails_helper'
RSpec.describe StatusStat, type: :model do
- pending "add some examples to (or delete) #{__FILE__}"
end