Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/purple-hairs-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes an issue where enterprise routing algorithms could get stuck on selecting the same agent due to chat limits being applied after agent selection, but before agent assignment
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class AutoSelection implements IRoutingMethod {
}

async getNextAgent(department?: string, ignoreAgentId?: string): Promise<SelectedAgent | null | undefined> {
// TODO: apply this extra query to other routing algorithms
const extraQuery = await callbacks.run('livechat.applySimultaneousChatRestrictions', undefined, {
...(department ? { departmentId: department } : {}),
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,43 @@
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import type { ILivechatDepartment, AvailableAgentsAggregation } from '@rocket.chat/core-typings';
import { LivechatDepartment } from '@rocket.chat/models';
import type { Document } from 'mongodb';
import type { Filter } from 'mongodb';

import { settings } from '../../../../../app/settings/server';
import { callbacks } from '../../../../../lib/callbacks';

callbacks.add(
'livechat.applySimultaneousChatRestrictions',
async (_: any, { departmentId }: { departmentId?: string } = {}) => {
const limitFilter: Document[] = [];
export async function getChatLimitsQuery(departmentId?: string): Promise<Filter<AvailableAgentsAggregation>> {
const limitFilter: Filter<AvailableAgentsAggregation> = [];

if (departmentId) {
const departmentLimit =
(
await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'maxNumberSimultaneousChat'>>(departmentId, {
projection: { maxNumberSimultaneousChat: 1 },
})
)?.maxNumberSimultaneousChat || 0;
if (departmentLimit > 0) {
limitFilter.push({ 'queueInfo.chatsForDepartment': { $gte: Number(departmentLimit) } });
}
if (departmentId) {
const departmentLimit =
(
await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'maxNumberSimultaneousChat'>>(departmentId, {
projection: { maxNumberSimultaneousChat: 1 },
})
)?.maxNumberSimultaneousChat || 0;
if (departmentLimit > 0) {
limitFilter.push({ 'queueInfo.chatsForDepartment': { $gte: Number(departmentLimit) } });
}
}

limitFilter.push({
$and: [{ maxChatsForAgent: { $gt: 0 } }, { $expr: { $gte: ['$queueInfo.chats', '$maxChatsForAgent'] } }],
});

const maxChatsPerSetting = settings.get<number>('Livechat_maximum_chats_per_agent');
if (maxChatsPerSetting > 0) {
limitFilter.push({
$and: [{ maxChatsForAgent: { $gt: 0 } }, { $expr: { $gte: ['$queueInfo.chats', '$maxChatsForAgent'] } }],
$and: [{ maxChatsForAgent: { $eq: 0 } }, { 'queueInfo.chats': { $gte: maxChatsPerSetting } }],
});
}

const maxChatsPerSetting = settings.get<number>('Livechat_maximum_chats_per_agent');
if (maxChatsPerSetting > 0) {
limitFilter.push({
$and: [{ maxChatsForAgent: { $eq: 0 } }, { 'queueInfo.chats': { $gte: maxChatsPerSetting } }],
});
}
return { $match: { $or: limitFilter } };
}

return { $match: { $or: limitFilter } };
callbacks.add(
'livechat.applySimultaneousChatRestrictions',
async (_: any, { departmentId }: { departmentId?: string } = {}) => {
return getChatLimitsQuery(departmentId);
},
callbacks.priority.HIGH,
'livechat-apply-simultaneous-restrictions',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Users } from '@rocket.chat/models';
import { RoutingManager } from '../../../../../../app/livechat/server/lib/RoutingManager';
import { settings } from '../../../../../../app/settings/server';
import type { IRoutingManagerConfig } from '../../../../../../definition/IRoutingManagerConfig';
import { getChatLimitsQuery } from '../../hooks/applySimultaneousChatsRestrictions';

/* Load Balancing Queuing method:
*
Expand All @@ -29,10 +30,13 @@ class LoadBalancing {
}

async getNextAgent(department?: string, ignoreAgentId?: string) {
const extraQuery = await getChatLimitsQuery(department);
const unavailableUsers = await Users.getUnavailableAgents(department, extraQuery);
const nextAgent = await Users.getNextLeastBusyAgent(
department,
ignoreAgentId,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
unavailableUsers.map((u) => u.username),
);
if (!nextAgent) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Users } from '@rocket.chat/models';
import { RoutingManager } from '../../../../../../app/livechat/server/lib/RoutingManager';
import { settings } from '../../../../../../app/settings/server';
import type { IRoutingManagerConfig } from '../../../../../../definition/IRoutingManagerConfig';
import { getChatLimitsQuery } from '../../hooks/applySimultaneousChatsRestrictions';

/* Load Rotation Queuing method:
* Routing method where the agent with the oldest routing time is the next agent to serve incoming chats
Expand All @@ -28,10 +29,13 @@ class LoadRotation {
}

public async getNextAgent(department?: string, ignoreAgentId?: string): Promise<IOmnichannelCustomAgent | undefined> {
const extraQuery = await getChatLimitsQuery(department);
const unavailableUsers = await Users.getUnavailableAgents(department, extraQuery);
const nextAgent = await Users.getLastAvailableAgentRouted(
department,
ignoreAgentId,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
unavailableUsers.map((user) => user.username),
);
if (!nextAgent?.username) {
return;
Expand Down
20 changes: 11 additions & 9 deletions apps/meteor/ee/server/models/raw/Users.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import type { RocketChatRecordDeleted, IUser } from '@rocket.chat/core-typings';
import type { RocketChatRecordDeleted, IUser, AvailableAgentsAggregation } from '@rocket.chat/core-typings';
import { UsersRaw } from '@rocket.chat/models';
import type { Db, Collection } from 'mongodb';
import type { Db, Collection, Filter } from 'mongodb';

import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred';

type AgentMetadata = {
username: string;
};

declare module '@rocket.chat/model-typings' {
interface IUsersModel {
getUnavailableAgents(departmentId: string, customFilter: { [k: string]: any }[]): Promise<AgentMetadata[]>;
getUnavailableAgents(
departmentId: string,
customFilter: Filter<AvailableAgentsAggregation>,
): Promise<Pick<AvailableAgentsAggregation, 'username'>[]>;
}
}

Expand All @@ -19,7 +18,10 @@ export class UsersEE extends UsersRaw {
super(db, trash);
}

getUnavailableAgents(departmentId: string, customFilter: { [k: string]: any }[]): Promise<AgentMetadata[]> {
getUnavailableAgents(
departmentId: string,
customFilter: Filter<AvailableAgentsAggregation>,
): Promise<Pick<AvailableAgentsAggregation, 'username'>[]> {
// if department is provided, remove the agents that are not from the selected department
const departmentFilter = departmentId
? [
Expand All @@ -46,7 +48,7 @@ export class UsersEE extends UsersRaw {
: [];

return this.col
.aggregate<AgentMetadata>(
.aggregate<AvailableAgentsAggregation>(
[
{
$match: {
Expand Down
Loading
Loading