0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 05:42:36 +08:00
discourse/app/jobs/regular/prepare_nested_reply_stats.rb
Mark VanLandingham c85593eda8
PERF: Add fast nested reply stats preparation (#41870)
## Problem

Nested reply statistics currently start backfilling only after nested
replies are enabled. At the default 100 topics every five minutes, that
is 28,800 topics per day—about 174 days for a site with five million
topics. Large sites therefore have to expose the feature before its
historical stats are ready.

## Solution

Add a backend-only `nested_replies:prepare_stats` rake task. For each
site, it:

- enables the existing realtime stats callbacks while the nested-replies
UI remains disabled;
- captures the current regular-topic high-water mark;
- enqueues low-priority, keyset-paginated batches which immediately
enqueue the next batch instead of waiting five minutes;
- reuses the existing per-topic backfill SQL; and
- isolates an individual failed topic into its own job, where normal
Sidekiq retries apply, so later topics can continue.

The task leaves the hidden maintenance flag enabled, so replies created,
deleted, or reparented after preparation begins keep the same stats
current before nested replies are enabled. On multisite it prepares
every database by default; `RAILS_DB` scopes it to one database.

The existing scheduled backfill keeps its original feature gate, topic
selection, batch schedule, and calculations. This adds no UI,
migrations, stats version, readiness marker, durable cursor setting,
lock, or watchdog.

Run it with:

```sh
RAILS_ENV=production bin/rake nested_replies:prepare_stats
```

Completion is logged with the captured high-water topic ID. Re-running
the task is safe because it uses the existing idempotent upsert
behavior.

Tests cover disabled-mode preparation, bounded continuation and
high-water behavior, isolated retries, rake-task enqueueing,
preservation of the old scheduler gate, and realtime maintenance while
the UI is disabled.
2026-07-21 10:45:53 -05:00

83 lines
2.4 KiB
Ruby
Vendored

# frozen_string_literal: true
module Jobs
class PrepareNestedReplyStats < ::Jobs::Base
sidekiq_options queue: "low"
cluster_concurrency 1
def execute(args = {})
raise Discourse::ReadOnly if Discourse.readonly_mode?
args ||= {}
isolated_topic_id = args[:topic_id].to_i
if isolated_topic_id.positive?
Jobs::BackfillNestedReplyStats.backfill_topic(isolated_topic_id)
return
end
after_topic_id = args[:after_topic_id].to_i
max_topic_id = args[:max_topic_id].to_i
max_topic_id = maximum_topic_id if max_topic_id.zero?
if after_topic_id >= max_topic_id
log_completion(max_topic_id)
return
end
topic_ids = topic_ids_to_prepare(after_topic_id:, max_topic_id:)
if topic_ids.empty?
log_completion(max_topic_id)
return
end
topic_ids.each do |topic_id|
Jobs::BackfillNestedReplyStats.backfill_topic(topic_id)
rescue => error
Discourse.warn_exception(
error,
message: "Failed to prepare nested reply stats for topic #{topic_id}",
)
Jobs.enqueue(:prepare_nested_reply_stats, topic_id: topic_id)
end
after_topic_id = topic_ids.last
if after_topic_id < max_topic_id
Jobs.enqueue(:prepare_nested_reply_stats, after_topic_id:, max_topic_id:)
else
log_completion(max_topic_id)
end
end
private
def maximum_topic_id
Topic.where(archetype: Archetype.default, deleted_at: nil).maximum(:id).to_i
end
def topic_ids_to_prepare(after_topic_id:, max_topic_id:)
DB.query_single(
<<~SQL,
SELECT topics.id
FROM topics
INNER JOIN posts op
ON op.topic_id = topics.id
AND op.post_number = 1
WHERE topics.id > :after_topic_id
AND topics.id <= :max_topic_id
AND topics.deleted_at IS NULL
AND topics.archetype = :archetype
ORDER BY topics.id
LIMIT :batch_size
SQL
after_topic_id: after_topic_id,
max_topic_id: max_topic_id,
archetype: Archetype.default,
batch_size: SiteSetting.nested_replies_backfill_batch_size,
)
end
def log_completion(max_topic_id)
Rails.logger.info("Nested reply stats preparation completed through topic #{max_topic_id}")
end
end
end