feat: Add sidekiq jobs to monitor applied SLAs (#8828)

Fixes: https://linear.app/chatwoot/issue/CW-2983/sidekiq-jobservice-to-monitor-sla-breach

Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
Vishnu Narayanan
2024-02-07 23:14:56 +05:30
committed by GitHub
parent 98eddd0532
commit c1d07a5471
17 changed files with 371 additions and 29 deletions

View File

@@ -0,0 +1,11 @@
module Enterprise::TriggerScheduledItemsJob
def perform
super
## Triggers Enterprise specific jobs
####################################
# Triggers Account Sla jobs
Sla::TriggerSlasForAccountsJob.perform_later
end
end

View File

@@ -0,0 +1,9 @@
class Sla::ProcessAccountAppliedSlasJob < ApplicationJob
queue_as :medium
def perform(account)
account.applied_slas.where(sla_status: 'active').each do |applied_sla|
Sla::ProcessAppliedSlaJob.perform_later(applied_sla)
end
end
end

View File

@@ -0,0 +1,7 @@
class Sla::ProcessAppliedSlaJob < ApplicationJob
queue_as :medium
def perform(applied_sla)
Sla::EvaluateAppliedSlaService.new(applied_sla: applied_sla).perform
end
end

View File

@@ -0,0 +1,10 @@
class Sla::TriggerSlasForAccountsJob < ApplicationJob
queue_as :scheduled_jobs
def perform
Account.find_each do |account|
Rails.logger.info "Enqueuing ProcessAccountAppliedSlasJob for account #{account.id}"
Sla::ProcessAccountAppliedSlasJob.perform_later(account)
end
end
end

View File

@@ -3,7 +3,7 @@
# Table name: applied_slas
#
# id :bigint not null, primary key
# sla_status :string
# sla_status :integer default("active")
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
@@ -20,4 +20,6 @@ class AppliedSla < ApplicationRecord
belongs_to :account
belongs_to :sla_policy
belongs_to :conversation
enum sla_status: { active: 0, hit: 1, missed: 2 }
end

View File

@@ -3,6 +3,7 @@ module Enterprise::Concerns::Account
included do
has_many :sla_policies, dependent: :destroy_async
has_many :applied_slas, dependent: :destroy_async
def self.add_response_related_associations
has_many :response_sources, dependent: :destroy_async

View File

@@ -0,0 +1,78 @@
class Sla::EvaluateAppliedSlaService
pattr_initialize [:applied_sla!]
def perform
check_sla_thresholds
# We will calculate again in the next iteration
return unless applied_sla.conversation.resolved?
# No SLA missed, so marking as hit as conversation is resolved
handle_hit_sla(applied_sla) if applied_sla.active?
end
private
def check_sla_thresholds
[:first_response_time_threshold, :next_response_time_threshold, :resolution_time_threshold].each do |threshold|
next if applied_sla.sla_policy.send(threshold).blank?
send("check_#{threshold}", applied_sla, applied_sla.conversation, applied_sla.sla_policy)
end
end
def still_within_threshold?(threshold)
Time.zone.now.to_i < threshold
end
def check_first_response_time_threshold(applied_sla, conversation, sla_policy)
threshold = conversation.created_at.to_i + sla_policy.first_response_time_threshold.to_i
return if first_reply_was_within_threshold?(conversation, threshold)
return if still_within_threshold?(threshold)
handle_missed_sla(applied_sla)
end
def first_reply_was_within_threshold?(conversation, threshold)
conversation.first_reply_created_at.present? && conversation.first_reply_created_at.to_i <= threshold
end
def check_next_response_time_threshold(applied_sla, conversation, sla_policy)
# still waiting for first reply, so covered under first response time threshold
return if conversation.first_reply_created_at.blank?
# Waiting on customer response, no need to check next response time threshold
return if conversation.waiting_since.blank?
threshold = conversation.waiting_since.to_i + sla_policy.next_response_time_threshold.to_i
return if still_within_threshold?(threshold)
handle_missed_sla(applied_sla)
end
def check_resolution_time_threshold(applied_sla, conversation, sla_policy)
return if conversation.resolved?
threshold = conversation.created_at.to_i + sla_policy.resolution_time_threshold.to_i
return if still_within_threshold?(threshold)
handle_missed_sla(applied_sla)
end
def handle_missed_sla(applied_sla)
return unless applied_sla.active?
applied_sla.update!(sla_status: 'missed')
Rails.logger.warn "SLA missed for conversation #{applied_sla.conversation.id} " \
"in account #{applied_sla.account_id} " \
"for sla_policy #{applied_sla.sla_policy.id}"
end
def handle_hit_sla(applied_sla)
return unless applied_sla.active?
applied_sla.update!(sla_status: 'hit')
Rails.logger.info "SLA hit for conversation #{applied_sla.conversation.id} " \
"in account #{applied_sla.account_id} " \
"for sla_policy #{applied_sla.sla_policy.id}"
end
end