Skip to content

Commit

Permalink
Chore: Refactor round robin logic (#1015)
Browse files Browse the repository at this point in the history
Co-authored-by: Pranav Raj S <pranav@thoughtwoot.com>
  • Loading branch information
sojan-official and pranavrajs committed Jul 7, 2020
1 parent 49db9c5 commit 0fc0dc1
Show file tree
Hide file tree
Showing 18 changed files with 192 additions and 69 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Expand Up @@ -39,6 +39,8 @@ Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
Style/RedundantFetchBlock:
Enabled: true
Style/RedundantRegexpCharacterClass:
Enabled: true
Style/RedundantRegexpEscape:
Expand Down
18 changes: 0 additions & 18 deletions .rubocop_todo.yml
Expand Up @@ -266,24 +266,6 @@ Style/CommentedKeyword:
- 'app/controllers/api/v1/conversations/labels_controller.rb'
- 'app/controllers/api/v1/labels_controller.rb'

# Offense count: 1
# Configuration parameters: EnforcedStyle.
# SupportedStyles: annotated, template, unannotated
Style/FormatStringToken:
Exclude:
- 'lib/constants/redis_keys.rb'

# Offense count: 4
# Configuration parameters: AllowedVariables.
Style/GlobalVars:
Exclude:
- 'lib/redis/alfred.rb'

# Offense count: 4
Style/IdenticalConditionalBranches:
Exclude:
- 'app/controllers/api/v1/reports_controller.rb'

# Offense count: 1
# Configuration parameters: AllowIfModifier.
Style/IfInsideElse:
Expand Down
2 changes: 1 addition & 1 deletion app/models/conversation.rb
Expand Up @@ -188,7 +188,7 @@ def run_round_robin
return unless conversation_status_changed_to_open?
return unless should_round_robin?

inbox.next_available_agent.then { |new_assignee| update_assignee(new_assignee) }
::RoundRobin::AssignmentService.new(conversation: self).perform
end

def create_status_change_message(user_name)
Expand Down
11 changes: 1 addition & 10 deletions app/models/inbox.rb
Expand Up @@ -64,11 +64,6 @@ def web_widget?
channel.class.name.to_s == 'Channel::WebWidget'
end

def next_available_agent
user_id = Redis::Alfred.rpoplpush(round_robin_key, round_robin_key)
account.users.find_by(id: user_id)
end

def webhook_data
{
id: id,
Expand All @@ -79,10 +74,6 @@ def webhook_data
private

def delete_round_robin_agents
Redis::Alfred.delete(round_robin_key)
end

def round_robin_key
format(Constants::RedisKeys::ROUND_ROBIN_AGENTS, inbox_id: id)
::RoundRobin::ManageService.new(inbox: self).clear_queue
end
end
8 changes: 2 additions & 6 deletions app/models/inbox_member.rb
Expand Up @@ -26,14 +26,10 @@ class InboxMember < ApplicationRecord
private

def add_agent_to_round_robin
Redis::Alfred.lpush(round_robin_key, user_id)
::RoundRobin::ManageService.new(inbox: inbox).add_agent_to_queue(user_id)
end

def remove_agent_from_round_robin
Redis::Alfred.lrem(round_robin_key, user_id)
end

def round_robin_key
format(Constants::RedisKeys::ROUND_ROBIN_AGENTS, inbox_id: inbox_id)
::RoundRobin::ManageService.new(inbox: inbox).remove_agent_from_queue(user_id)
end
end
5 changes: 4 additions & 1 deletion app/models/message.rb
Expand Up @@ -153,7 +153,6 @@ def execute_message_template_hooks
end

def notify_via_mail
conversation_mail_key = Redis::Alfred::CONVERSATION_MAILER_KEY % conversation.id
if Redis::Alfred.get(conversation_mail_key).nil? && conversation.contact.email? && outgoing?
# set a redis key for the conversation so that we don't need to send email for every
# new message that comes in and we dont enque the delayed sidekiq job for every message
Expand All @@ -165,6 +164,10 @@ def notify_via_mail
end
end

def conversation_mail_key
format(::Redis::Alfred::CONVERSATION_MAILER_KEY, conversation_id: conversation.id)
end

def validate_attachments_limit(_attachment)
errors.add(attachments: 'exceeded maximum allowed') if attachments.size >= NUMBER_OF_PERMITTED_ATTACHMENTS
end
Expand Down
24 changes: 24 additions & 0 deletions app/services/round_robin/assignment_service.rb
@@ -0,0 +1,24 @@
class RoundRobin::AssignmentService
pattr_initialize [:conversation]

def perform
# online agents will get priority
new_assignee = round_robin_manage_service.available_agent(priority_list: online_agents)
conversation.update(assignee: new_assignee) if new_assignee
end

private

def online_agents
online_agents = OnlineStatusTracker.get_available_users(conversation.account_id)
online_agents.select { |_key, value| value.eql?('online') }.keys if online_agents.present?
end

def round_robin_manage_service
@round_robin_manage_service ||= RoundRobin::ManageService.new(inbox: conversation.inbox)
end

def round_robin_key
format(::Redis::Alfred::ROUND_ROBIN_AGENTS, inbox_id: conversation.inbox_id)
end
end
56 changes: 56 additions & 0 deletions app/services/round_robin/manage_service.rb
@@ -0,0 +1,56 @@
class RoundRobin::ManageService
pattr_initialize [:inbox!]

# called on inbox delete
def clear_queue
::Redis::Alfred.delete(round_robin_key)
end

# called on inbox member create
def add_agent_to_queue(user_id)
::Redis::Alfred.lpush(round_robin_key, user_id)
end

# called on inbox member delete
def remove_agent_from_queue(user_id)
::Redis::Alfred.lrem(round_robin_key, user_id)
end

def available_agent(priority_list: [])
reset_queue unless validate_queue?
user_id = get_agent_via_priority_list(priority_list)
# incase priority list was empty or inbox members weren't present
user_id ||= ::Redis::Alfred.rpoplpush(round_robin_key, round_robin_key)
inbox.inbox_members.find_by(user_id: user_id)&.user if user_id.present?
end

def reset_queue
clear_queue
add_agent_to_queue(inbox.inbox_members.map(&:user_id))
end

private

def get_agent_via_priority_list(priority_list)
return if priority_list.blank?

user_id = queue.intersection(priority_list.map(&:to_s)).pop
if user_id.present?
remove_agent_from_queue(user_id)
add_agent_to_queue(user_id)
end
user_id
end

def validate_queue?
return true if inbox.inbox_members.map(&:user_id).sort == queue.sort.map(&:to_i)
end

def queue
::Redis::Alfred.lrange(round_robin_key)
end

def round_robin_key
format(::Redis::Alfred::ROUND_ROBIN_AGENTS, inbox_id: inbox.id)
end
end
7 changes: 6 additions & 1 deletion app/workers/conversation_reply_email_worker.rb
Expand Up @@ -9,7 +9,12 @@ def perform(conversation_id, queued_time)
ConversationReplyMailer.reply_with_summary(@conversation, queued_time).deliver_later

# delete the redis set from the first new message on the conversation
conversation_mail_key = Redis::Alfred::CONVERSATION_MAILER_KEY % @conversation.id
Redis::Alfred.delete(conversation_mail_key)
end

private

def conversation_mail_key
format(::Redis::Alfred::CONVERSATION_MAILER_KEY, conversation_id: @conversation.id)
end
end
3 changes: 2 additions & 1 deletion config/initializers/redis.rb
Expand Up @@ -4,8 +4,9 @@
}
redis = Rails.env.test? ? MockRedis.new : Redis.new(app_redis_config)

# Alfred - Used currently for round robin and conversation emails.
# Alfred
# Add here as you use it for more features
# Used for Round Robin, Conversation Emails & Online Presence
$alfred = Redis::Namespace.new('alfred', redis: redis, warning: true)

# https://github.com/mperham/sidekiq/issues/4591
Expand Down
4 changes: 2 additions & 2 deletions config/puma.rb
Expand Up @@ -4,13 +4,13 @@
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
max_threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 }
max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
threads min_threads_count, max_threads_count

# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port ENV.fetch('PORT') { 3000 }
port ENV.fetch('PORT', 3000)

# Specifies the `environment` that Puma will run in.
#
Expand Down
2 changes: 1 addition & 1 deletion db/migrate/20170525104650_round_robin.rb
@@ -1,7 +1,7 @@
class RoundRobin < ActiveRecord::Migration[5.0]
def change
InboxMember.find_each do |im|
round_robin_key = format(Constants::RedisKeys::ROUND_ROBIN_AGENTS, inbox_id: im.inbox_id)
round_robin_key = format(::Redis::Alfred::ROUND_ROBIN_AGENTS, inbox_id: im.inbox_id)
Redis::Alfred.lpush(round_robin_key, im.user_id)
end
end
Expand Down
3 changes: 0 additions & 3 deletions lib/constants/redis_keys.rb

This file was deleted.

6 changes: 3 additions & 3 deletions lib/online_status_tracker.rb
Expand Up @@ -16,9 +16,9 @@ def self.get_presence(account_id, obj_type, obj_id)
def self.presence_key(account_id, type)
case type
when 'Contact'
Redis::Alfred::ONLINE_PRESENCE_CONTACTS % account_id
format(::Redis::Alfred::ONLINE_PRESENCE_CONTACTS, account_id: account_id)
else
Redis::Alfred::ONLINE_PRESENCE_USERS % account_id
format(::Redis::Alfred::ONLINE_PRESENCE_USERS, account_id: account_id)
end
end

Expand All @@ -34,7 +34,7 @@ def self.get_status(account_id, user_id)
end

def self.status_key(account_id)
Redis::Alfred::ONLINE_STATUS % account_id
format(::Redis::Alfred::ONLINE_STATUS, account_id: account_id)
end

def self.get_available_contacts(account_id)
Expand Down
53 changes: 31 additions & 22 deletions lib/redis/alfred.rb
@@ -1,43 +1,52 @@
module Redis::Alfred
CONVERSATION_MAILER_KEY = 'CONVERSATION::%d'.freeze

# hash containing user_id key and status as value ONLINE_STATUS::%accountid
ONLINE_STATUS = 'ONLINE_STATUS::%s'.freeze
# sorted set storing online presense of account contacts : ONLINE_PRESENCE::%accountid::CONTACTS
ONLINE_PRESENCE_CONTACTS = 'ONLINE_PRESENCE::%s::CONTACTS'.freeze
# sorted set storing online presense of account users : ONLINE_PRESENCE::%accountid::USERS
ONLINE_PRESENCE_USERS = 'ONLINE_PRESENCE::%s::USERS'.freeze
include Redis::RedisKeys

class << self
def rpoplpush(source, destination)
$alfred.rpoplpush(source, destination)
# key operations

def set(key, value)
$alfred.set(key, value)
end

def lpush(key, value)
$alfred.lpush(key, value)
def setex(key, value, expiry = 1.day)
$alfred.setex(key, expiry, value)
end

def get(key)
$alfred.get(key)
end

def delete(key)
$alfred.del(key)
end

def lrem(key, value, count = 0)
$alfred.lrem(key, count, value)
# list operations

def llen(key)
$alfred.llen(key)
end

def setex(key, value, expiry = 1.day)
$alfred.setex(key, expiry, value)
def lrange(key, start_index = 0, end_index = -1)
$alfred.lrange(key, start_index, end_index)
end

def set(key, value)
$alfred.set(key, value)
def rpop(key)
$alfred.rpop(key)
end

def get(key)
$alfred.get(key)
def lpush(key, values)
$alfred.lpush(key, values)
end

def rpoplpush(source, destination)
$alfred.rpoplpush(source, destination)
end

def lrem(key, value, count = 0)
$alfred.lrem(key, count, value)
end

# hash operation
# hash operations

# add a key value to redis hash
def hset(key, field, value)
Expand All @@ -54,7 +63,7 @@ def hmget(key, fields)
$alfred.hmget(key, *fields)
end

# sorted set functions
# sorted set operations

# add score and value for a key
def zadd(key, score, value)
Expand Down
13 changes: 13 additions & 0 deletions lib/redis/redis_keys.rb
@@ -0,0 +1,13 @@
module Redis::RedisKeys
ROUND_ROBIN_AGENTS = 'ROUND_ROBIN_AGENTS:%<inbox_id>d'.freeze

CONVERSATION_MAILER_KEY = 'CONVERSATION::%<conversation_id>d'.freeze

## Online Status Keys
# hash containing user_id key and status as value
ONLINE_STATUS = 'ONLINE_STATUS::%<account_id>d'.freeze
# sorted set storing online presense of account contacts
ONLINE_PRESENCE_CONTACTS = 'ONLINE_PRESENCE::%<account_id>d::CONTACTS'.freeze
# sorted set storing online presense of account users
ONLINE_PRESENCE_USERS = 'ONLINE_PRESENCE::%<account_id>d::USERS'.freeze
end
3 changes: 3 additions & 0 deletions spec/models/conversation_spec.rb
Expand Up @@ -108,6 +108,7 @@
end

before do
create(:inbox_member, inbox: inbox, user: agent)
allow(Redis::Alfred).to receive(:rpoplpush).and_return(agent.id)
end

Expand Down Expand Up @@ -141,9 +142,11 @@
conversation.status = 'resolved'
conversation.save!
expect(conversation.reload.assignee).to eq(agent)
inbox.inbox_members.where(user_id: agent.id).first.destroy!

# round robin changes assignee in this case since agent doesn't have access to inbox
agent2 = create(:user, email: 'agent2@example.com', account: account)
create(:inbox_member, inbox: inbox, user: agent2)
allow(Redis::Alfred).to receive(:rpoplpush).and_return(agent2.id)
conversation.status = 'open'
conversation.save!
Expand Down

0 comments on commit 0fc0dc1

Please sign in to comment.