## Description Fixes a bug under Assignment V2 where a single conversation could be reassigned dozens of times in a row by the system, producing long stacks of "Assigned to X by Automation System via <policy>" activity messages alternating between agents. After this change each unassigned conversation is assigned exactly once, even on busy inboxes. ## Fixes # (issue) ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ## How to reproduce 1. Enable `assignment_v2` on an account with at least 2 online agents in an inbox. 2. Generate sustained resolve/snooze activity in the inbox (each one enqueues `AutoAssignment::AssignmentJob` for the whole inbox). 3. Watch any one unassigned conversation while the jobs drain — pre-fix it picks up multiple back-to-back "Assigned to …" activity rows alternating between agents. ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
55 lines
2.0 KiB
Ruby
55 lines
2.0 KiB
Ruby
class AutoAssignment::AssignmentJob < ApplicationJob
|
|
queue_as :default
|
|
|
|
IN_FLIGHT_TTL = 5.minutes
|
|
|
|
# Coalesce per inbox: at most one AssignmentJob per inbox is in-flight
|
|
# (queued or running) at any time. The marker carries a token so a job only
|
|
# releases its own claim (a newer job may have taken it after a TTL lapse).
|
|
def self.enqueue_for_inbox(inbox_id)
|
|
key = format(::Redis::Alfred::AUTO_ASSIGNMENT_IN_FLIGHT_KEY, inbox_id: inbox_id)
|
|
token = SecureRandom.uuid
|
|
return false unless ::Redis::Alfred.set(key, token, nx: true, ex: IN_FLIGHT_TTL)
|
|
|
|
return true if perform_later(inbox_id: inbox_id, token: token)
|
|
|
|
# Enqueue was halted; release our own claim so the inbox isn't gated until the TTL.
|
|
::Redis::Alfred.delete_if_equals(key, token)
|
|
false
|
|
rescue StandardError
|
|
# Enqueue raised after we claimed the gate; release our own claim, then re-raise.
|
|
::Redis::Alfred.delete_if_equals(key, token)
|
|
raise
|
|
end
|
|
|
|
def perform(inbox_id:, token: nil)
|
|
inbox = Inbox.find_by(id: inbox_id)
|
|
return unless inbox
|
|
|
|
service = AutoAssignment::AssignmentService.new(inbox: inbox)
|
|
assigned_count = service.perform_bulk_assignment(limit: bulk_assignment_limit)
|
|
Rails.logger.info "Assigned #{assigned_count} conversations for inbox #{inbox.id}"
|
|
rescue StandardError => e
|
|
Rails.logger.error "Bulk assignment failed for inbox #{inbox_id}: #{e.message}"
|
|
raise e if Rails.env.test?
|
|
ensure
|
|
release_in_flight(inbox_id, token)
|
|
end
|
|
|
|
private
|
|
|
|
# Release the in-flight marker only if we still own it. The atomic
|
|
# compare-and-delete ensures a job whose TTL lapsed can't delete a newer
|
|
# job's claim. Tokenless (pre-deploy) jobs never claimed a key, so skip.
|
|
def release_in_flight(inbox_id, token)
|
|
return if token.nil?
|
|
|
|
key = format(::Redis::Alfred::AUTO_ASSIGNMENT_IN_FLIGHT_KEY, inbox_id: inbox_id)
|
|
::Redis::Alfred.delete_if_equals(key, token)
|
|
end
|
|
|
|
def bulk_assignment_limit
|
|
ENV.fetch('AUTO_ASSIGNMENT_BULK_LIMIT', 100).to_i
|
|
end
|
|
end
|