discourse/app/models/topic_timer.rb
Martin Brennan 0034cbda8a
DEV: Change Topic Timer from enqueue_at scheduled jobs to incrementally executed jobs (#11698)
Moves the topic timer jobs from being scheduled ahead of time with enqueue_at to a 5 minute scheduled run like bookmark reminders, in a new job called Jobs::EnqueueTopicTimers. Backwards compatibility is maintained by checking if an existing topic timer job is enqueued in sidekiq for the timer, and if it is not running it inside the new job.

The functionality to close/open a topic if it is in the opposite state still remains in the after_save block of TopicTimer, with further commentary, which is used for Open/Close Temporarily.

This also removes the ensure_consistency! functionality of topic timers as it is no longer needed; the new job will always pick up the timers because they are not stored in a fragile state of sidekiq.
2021-01-19 13:30:58 +10:00

247 lines
7.7 KiB
Ruby

# frozen_string_literal: true
class TopicTimer < ActiveRecord::Base
include Trashable
belongs_to :user
belongs_to :topic
belongs_to :category
validates :user_id, presence: true
validates :topic_id, presence: true
validates :execute_at, presence: true
validates :status_type, presence: true
validates :status_type, uniqueness: { scope: [:topic_id, :deleted_at] }, if: :public_type?
validates :status_type, uniqueness: { scope: [:topic_id, :deleted_at, :user_id] }, if: :private_type?
validates :category_id, presence: true, if: :publishing_to_category?
validate :executed_at_in_future?
scope :scheduled_bump_topics, -> { where(status_type: TopicTimer.types[:bump], deleted_at: nil).pluck(:topic_id) }
scope :pending_timers, ->(before_time = Time.now.utc) do
where("execute_at <= :before_time AND deleted_at IS NULL", before_time: before_time)
end
before_save do
self.created_at ||= Time.zone.now if execute_at
self.public_type = self.public_type?
if (will_save_change_to_execute_at? &&
!attribute_in_database(:execute_at).nil?) ||
will_save_change_to_user_id?
# TODO(martin - 2021-05-01) - Remove this backwards compatability for outstanding
# jobs once they have all been run and after Jobs::TopicTimerEnqueuer is in place
self.send("cancel_auto_#{self.class.types[status_type]}_job")
end
end
# These actions are in place to make sure the topic is in the correct
# state at the point in time where the timer is saved. It does not
# guarantee that the topic will be in the correct state when the timer
# job is executed, but each timer job handles deleted topics etc. gracefully.
#
# This is also important for the Open Temporarily and Close Temporarily timers,
# which change the topic's status straight away and set a timer to do the
# opposite action in the future.
after_save do
if (saved_change_to_execute_at? || saved_change_to_user_id?)
if status_type == TopicTimer.types[:silent_close] || status_type == TopicTimer.types[:close]
topic.update_status('closed', false, user) if topic.closed?
end
if status_type == TopicTimer.types[:open]
topic.update_status('closed', true, user) if topic.open?
end
end
end
def status_type_name
self.class.types[status_type]
end
def enqueue_typed_job(time: nil)
return if typed_job_scheduled?
self.send("schedule_auto_#{status_type_name}_job")
end
# TODO(martin - 2021-05-01) - Remove this backwards compatability for outstanding
# jobs once they have all been run and after Jobs::TopicTimerEnqueuer is in place
def typed_job_scheduled?
scheduled = Jobs.scheduled_for(
TopicTimer.type_job_map[status_type_name], topic_timer_id: id
).any?
if [:close, :silent_close, :open].include?(status_type_name)
return scheduled || Jobs.scheduled_for(:toggle_topic_closed, topic_timer_id: id).any?
end
scheduled
end
def self.type_job_map
{
close: :close_topic,
open: :open_topic,
publish_to_category: :publish_topic_to_category,
delete: :delete_topic,
reminder: :topic_reminder,
bump: :bump_topic,
delete_replies: :delete_replies,
silent_close: :close_topic,
clear_slow_mode: :clear_slow_mode
}
end
def self.types
@types ||= Enum.new(
close: 1,
open: 2,
publish_to_category: 3,
delete: 4,
reminder: 5,
bump: 6,
delete_replies: 7,
silent_close: 8,
clear_slow_mode: 9
)
end
def self.public_types
@_public_types ||= types.except(:reminder, :clear_slow_mode)
end
def self.private_types
@_private_types ||= types.only(:reminder, :clear_slow_mode)
end
def public_type?
!!self.class.public_types[self.status_type]
end
def private_type?
!!self.class.private_types[self.status_type]
end
def runnable?
return false if deleted_at.present?
return false if execute_at > Time.zone.now
true
end
private
def executed_at_in_future?
return if created_at.blank? || (execute_at > created_at)
errors.add(:execute_at, I18n.t(
'activerecord.errors.models.topic_timer.attributes.execute_at.in_the_past'
))
end
def publishing_to_category?
self.status_type.to_i == TopicTimer.types[:publish_to_category]
end
# TODO(martin - 2021-05-01) - Remove cancels for toggle_topic_closed once topic timer revamp completed.
def cancel_auto_close_job
Jobs.cancel_scheduled_job(:toggle_topic_closed, topic_timer_id: id)
Jobs.cancel_scheduled_job(:close_topic, topic_timer_id: id)
end
# TODO(martin - 2021-05-01) - Remove cancels for toggle_topic_closed once topic timer revamp completed.
def cancel_auto_open_job
Jobs.cancel_scheduled_job(:toggle_topic_closed, topic_timer_id: id)
Jobs.cancel_scheduled_job(:open_topic, topic_timer_id: id)
end
# TODO(martin - 2021-05-01) - Remove cancels for toggle_topic_closed once topic timer revamp completed.
def cancel_auto_silent_close_job
Jobs.cancel_scheduled_job(:toggle_topic_closed, topic_timer_id: id)
Jobs.cancel_scheduled_job(:close_topic, topic_timer_id: id)
end
def cancel_auto_publish_to_category_job
Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:publish_to_category], topic_timer_id: id)
end
def cancel_auto_delete_job
Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:delete], topic_timer_id: id)
end
def cancel_auto_reminder_job
Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:reminder], topic_timer_id: id)
end
def cancel_auto_bump_job
Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:bump], topic_timer_id: id)
end
def cancel_auto_delete_replies_job
Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:delete_replies], topic_timer_id: id)
end
def cancel_auto_clear_slow_mode_job
Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:clear_slow_mode], topic_timer_id: id)
end
def schedule_auto_delete_replies_job
Jobs.enqueue(TopicTimer.type_job_map[:delete_replies], topic_timer_id: id)
end
def schedule_auto_bump_job
Jobs.enqueue(TopicTimer.type_job_map[:bump], topic_timer_id: id)
end
def schedule_auto_open_job
Jobs.enqueue(TopicTimer.type_job_map[:open], topic_timer_id: id)
end
def schedule_auto_close_job
Jobs.enqueue(TopicTimer.type_job_map[:close], topic_timer_id: id)
end
def schedule_auto_silent_close_job
Jobs.enqueue(TopicTimer.type_job_map[:close], topic_timer_id: id, silent: true)
end
def schedule_auto_publish_to_category_job
Jobs.enqueue(TopicTimer.type_job_map[:publish_to_category], topic_timer_id: id)
end
def schedule_auto_delete_job
Jobs.enqueue(TopicTimer.type_job_map[:delete], topic_timer_id: id)
end
def schedule_auto_reminder_job
# noop, TODO(martin 2021-03-11): Remove this after timers migrated and outstanding jobs cancelled
end
def schedule_auto_clear_slow_mode_job
Jobs.enqueue(TopicTimer.type_job_map[:clear_slow_mode], topic_timer_id: id)
end
end
# == Schema Information
#
# Table name: topic_timers
#
# id :integer not null, primary key
# execute_at :datetime not null
# status_type :integer not null
# user_id :integer not null
# topic_id :integer not null
# based_on_last_post :boolean default(FALSE), not null
# deleted_at :datetime
# deleted_by_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# category_id :integer
# public_type :boolean default(TRUE)
# duration :integer
#
# Indexes
#
# idx_topic_id_public_type_deleted_at (topic_id) UNIQUE WHERE ((public_type = true) AND (deleted_at IS NULL))
# index_topic_timers_on_user_id (user_id)
#