ActiveRecord Models: Where Business Logic Lives
Topic and Post are the two central ActiveRecord models in Discourse: Topic owns the thread (title, category, status, thumbnails) and Post owns each reply (cooking raw text to HTML, revisions, moderation). Their before_save/after_save/after_commit callbacks are the wiring that turns a plain database write into async side effects — enqueuing Sidekiq jobs, reindexing search, and publishing MessageBus events that push live updates to connected browsers. Because nearly every controller, serializer, job, and plugin touches these two models directly, understanding their callback chains is the fastest way to understand how the rest of the system reacts to a single save.
Post#trigger_post_process: handing work off to a background job
app/models/post.rb · lines 982–1003Called after a post is created or rebaked. Options let the caller tune the job: whether to bypass topic bumping, run at a lower priority, mark it as a brand-new post, or skip pulling hotlinked images.
Builds the payload that will be serialized and handed to the job queue — note it only carries the post_id, not the model itself, since the job runs in a separate process/worker.
Jobs.enqueue hands the payload to Sidekiq — the actual cooking/onebox/thumbnail work happens later, asynchronously. DiscourseEvent.trigger runs synchronously in-process right here, giving plugins a hook without waiting on the job queue.
# Enqueue post processing for this post def trigger_post_process( bypass_bump: false, priority: :normal, new_post: false, skip_pull_hotlinked_images: false ) args = { bypass_bump: bypass_bump, cooking_options: cooking_options, new_post: new_post, post_id: id, skip_pull_hotlinked_images: skip_pull_hotlinked_images, } args[:image_sizes] = image_sizes if image_sizes.present? args[:invalidate_oneboxes] = true if invalidate_oneboxes.present? args[:queue] = priority.to_s if priority && priority != :normal Jobs.enqueue(:process_post, args) DiscourseEvent.trigger(:after_trigger_post_process, self) end
def publish_change_to_clients!(type, opts = {}) # special failsafe for posts missing topics consistency checks should fix, # but message is safe to skip return unless topic skip_topic_stats = opts.delete(:skip_topic_stats) message = { id: id, post_number: post_number, updated_at: Time.now, user_id: user_id, last_editor_id: last_editor_id, type: type, version: version, }.merge(opts) publish_message!("/topic/#{topic_id}", message) Topic.publish_stats_to_clients!(topic.id, type) unless skip_topic_stats end def publish_message!(channel, message, opts = {}) return unless topic if Topic.visible_post_types.include?(post_type) opts.merge!(topic.secure_audience_publish_messages) else opts[:user_ids] = User.human_users.where("admin OR moderator OR id = ?", user_id).pluck(:id) end MessageBus.publish(channel, message, opts) if opts[:user_ids] != [] && opts[:group_ids] != [] end
app/models/post.rb · lines 222–253Topic's after_save: reindexing and tag bookkeeping
app/models/topic.rb · lines 425–444If the topic just became (or stopped being) a banner, the cached banner JSON is invalidated so the next page load picks up the change.
Title, category, or tag changes queue a search reindex, and if tags changed, TagUser watch/track state is recalculated for users who follow those tags — the tags_changed flag is a manual attr_accessor set elsewhere and reset here.
Every save re-indexes the topic for search regardless of which attribute changed — a simple but broad-brush way to keep search data fresh.
after_save do banner = "banner" if archetype_before_last_save == banner || archetype == banner ApplicationLayoutPreloader.banner_json_cache.clear end if tags_changed || saved_change_to_attribute?(:category_id) || saved_change_to_attribute?(:title) SearchIndexer.queue_post_reindex(id) if tags_changed TagUser.auto_watch(topic_id: id) TagUser.auto_track(topic_id: id) self.tags_changed = false end end SearchIndexer.index(self) end
Check your understanding
Answered in place — nothing is graded, everything is explained. 0 / 3 passed
What does Post#trigger_post_process actually do?
Why does Topic#changed_to_category wrap its Jobs.enqueue calls in DB.after_commit blocks?
Which call is responsible for the live, real-time push to browsers viewing a topic when a post changes?