fix: atomically claim conversation to prevent duplicate assignment (#14495)

## 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
This commit is contained in:
Tanmay Deep Sharma
2026-05-21 16:14:28 +05:30
committed by GitHub
parent f33e469e9a
commit 3cd8cf43ce
8 changed files with 107 additions and 17 deletions
+12
View File
@@ -25,6 +25,18 @@ module Redis::Alfred
$alfred.with { |conn| conn.del(key) }
end
# atomic compare-and-delete (release a lock only if you still own it); WATCH/MULTI
# aborts the delete if the key changes between the check and the delete.
def delete_if_equals(key, expected_value)
$alfred.with do |conn|
conn.watch(key) do
next conn.unwatch unless conn.get(key) == expected_value
conn.multi { |transaction| transaction.del(key) }
end
end
end
# increment a key by 1. throws error if key value is incompatible
# sets key to 0 before operation if key doesn't exist
def incr(key)
+2
View File
@@ -73,6 +73,8 @@ module Redis::RedisKeys
# Track conversation assignments to agents for rate limiting
ASSIGNMENT_KEY = 'ASSIGNMENT::%<inbox_id>d::AGENT::%<agent_id>d::CONVERSATION::%<conversation_id>d'.freeze
ASSIGNMENT_KEY_PATTERN = 'ASSIGNMENT::%<inbox_id>d::AGENT::%<agent_id>d::*'.freeze
# At-most-one AssignmentJob per inbox in-flight (queued or running); further enqueues are skipped
AUTO_ASSIGNMENT_IN_FLIGHT_KEY = 'AUTO_ASSIGNMENT_IN_FLIGHT::%<inbox_id>d'.freeze
## Account Onboarding
ACCOUNT_ONBOARDING_ENRICHMENT = 'ONBOARDING_ENRICHMENT::%<account_id>d'.freeze