From f8ca0cad5d9153ab13e5dbc6c22cba604734731e Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 16 Jan 2025 09:01:02 -0600 Subject: [PATCH 1/7] Make rooom opening transactional --- apps/meteor/app/livechat/server/lib/Helper.ts | 23 ++--- .../app/livechat/server/lib/QueueManager.ts | 87 +++++++++++++------ .../src/models/ILivechatRoomsModel.ts | 14 ++- packages/models/src/models/LivechatRooms.ts | 5 +- 4 files changed, 89 insertions(+), 40 deletions(-) diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index b6a07024c9898..74204769661ab 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -34,7 +34,7 @@ import { } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { ObjectId } from 'mongodb'; +import { ClientSession, ObjectId } from 'mongodb'; import { Livechat as LivechatTyped } from './LivechatTyped'; import { queueInquiry, saveQueueInquiry } from './QueueManager'; @@ -67,12 +67,12 @@ export const allowAgentSkipQueue = (agent: SelectedAgent) => { return hasRoleAsync(agent.agentId, 'bot'); }; -export const createLivechatRoom = async ( +export const prepareLivechatRoom = async ( rid: string, guest: ILivechatVisitor, roomInfo: IOmnichannelRoomInfo = { source: { type: OmnichannelSourceType.OTHER } }, extraData?: IOmnichannelRoomExtraData, -): Promise => { +): Promise> => { check(rid, String); check( guest, @@ -112,7 +112,7 @@ export const createLivechatRoom = async ( const verified = Boolean(contact.channels.some((channel) => isVerifiedChannelInSource(channel, _id, source))); // TODO: Solve `u` missing issue - const room: InsertionModel = { + return { _id: rid, msgs: 0, usersCount: 1, @@ -145,8 +145,10 @@ export const createLivechatRoom = async ( estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE, ...extraRoomInfo, } as InsertionModel; +}; - const result = await Rooms.findOneAndUpdate( +export const createLivechatRoom = async (room: InsertionModel, session: ClientSession) => { + const result = await LivechatRooms.findOneAndUpdate( room, { $set: {}, @@ -154,6 +156,7 @@ export const createLivechatRoom = async ( { upsert: true, returnDocument: 'after', + session, }, ); @@ -161,10 +164,7 @@ export const createLivechatRoom = async ( throw new Error('Room not created'); } - await callbacks.run('livechat.newRoom', room); - await Message.saveSystemMessageAndNotifyUser('livechat-started', rid, '', { _id, username }, { groupable: false, token: guest.token }); - - return result as IOmnichannelRoom; + return result; }; export const createLivechatInquiry = async ({ @@ -174,6 +174,7 @@ export const createLivechatInquiry = async ({ message, initialStatus, extraData, + session, }: { rid: string; name?: string; @@ -181,6 +182,7 @@ export const createLivechatInquiry = async ({ message?: string; initialStatus?: LivechatInquiryStatus; extraData?: IOmnichannelInquiryExtraData; + session?: ClientSession; }) => { check(rid, String); check(name, String); @@ -202,7 +204,7 @@ export const createLivechatInquiry = async ({ const ts = new Date(); logger.debug({ - msg: `Creating livechat inquiry for visitor ${_id}`, + msg: `Creating livechat inquiry for visitor`, visitor: { _id, username, department, status, activity }, }); @@ -235,6 +237,7 @@ export const createLivechatInquiry = async ({ { upsert: true, returnDocument: 'after', + session, }, ); logger.debug(`Inquiry ${result} created for visitor ${_id}`); diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 2e846bc24d9ef..83cdbde098c79 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -1,5 +1,5 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; -import { Omnichannel } from '@rocket.chat/core-services'; +import { Message, Omnichannel } from '@rocket.chat/core-services'; import type { ILivechatDepartment, IOmnichannelRoomInfo, @@ -17,7 +17,7 @@ import { Random } from '@rocket.chat/random'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { createLivechatRoom, createLivechatInquiry, allowAgentSkipQueue } from './Helper'; +import { createLivechatRoom, createLivechatInquiry, allowAgentSkipQueue, prepareLivechatRoom } from './Helper'; import { Livechat } from './LivechatTyped'; import { RoutingManager } from './RoutingManager'; import { isVerifiedChannelInSource } from './contacts/isVerifiedChannelInSource'; @@ -34,6 +34,8 @@ import { import { settings } from '../../../settings/server'; import { i18n } from '../../../utils/lib/i18n'; import { getOmniChatSortQuery } from '../../lib/inquiries'; +import { InsertionModel } from '@rocket.chat/model-typings'; +import { client, shouldRetryTransaction } from '/server/database/utils'; const logger = new Logger('QueueManager'); @@ -213,6 +215,51 @@ export class QueueManager { return Boolean(contact.channels.some((channel) => isVerifiedChannelInSource(channel, room.v._id, room.source))); } + static async startConversation( + rid: string, + insertionRoom: InsertionModel, + guest: ILivechatVisitor, + roomInfo: IOmnichannelRoomInfo, + defaultAgent?: SelectedAgent, + message?: string, + extraData?: IOmnichannelRoomExtraData, + attempts = 3, + ): Promise<{ room: IOmnichannelRoom; inquiry: ILivechatInquiryRecord }> { + const session = client.startSession(); + try { + session.startTransaction(); + const room = await createLivechatRoom(insertionRoom, session); + logger.debug(`Room for visitor ${guest._id} created with id ${room._id}`); + const inquiry = await createLivechatInquiry({ + rid, + name: room.fname, + initialStatus: await this.getInquiryStatus({ room, agent: defaultAgent }), + guest, + message, + extraData: { ...extraData, source: roomInfo.source }, + session, + }); + const livechatSetting = await LivechatRooms.updateRoomCount(session); + if (livechatSetting) { + void notifyOnSettingChanged(livechatSetting); + } + await session.commitTransaction(); + return { room, inquiry }; + } catch (e) { + await session.abortTransaction(); + if (shouldRetryTransaction(e)) { + if (attempts > 0) { + logger.debug({ msg: 'Retrying transaction because of transient error', attemptsLeft: attempts }); + return this.startConversation(rid, insertionRoom, guest, roomInfo, defaultAgent, message, extraData, attempts - 1); + } + throw new Error('error-failed-to-start-conversation'); + } + throw e; + } finally { + await session.endSession(); + } + } + static async requestRoom({ guest, rid = Random.id(), @@ -280,38 +327,25 @@ export class QueueManager { } } - const room = await createLivechatRoom(rid, { ...guest, ...(department && { department }) }, roomInfo, { + const insertionRoom = await prepareLivechatRoom(rid, { ...guest, ...(department && { department }) }, roomInfo, { ...extraData, ...(Boolean(customFields) && { customFields }), }); - if (!room) { - logger.error(`Room for visitor ${guest._id} not found`); - throw new Error('room-not-found'); - } - logger.debug(`Room for visitor ${guest._id} created with id ${room._id}`); + // Transactional start of the conversation. This should prevent rooms from being created without inquiries and viceversa. + // All the actions that happened inside createLivechatRoom are now outside this transaction + const { room, inquiry } = await this.startConversation(rid, insertionRoom, guest, roomInfo, defaultAgent, message, extraData); - const inquiry = await createLivechatInquiry({ + await callbacks.run('livechat.newRoom', room); + await Message.saveSystemMessageAndNotifyUser( + 'livechat-started', rid, - name: room.fname, - initialStatus: await this.getInquiryStatus({ room, agent: defaultAgent }), - guest, - message, - extraData: { ...extraData, source: roomInfo.source }, - }); - - if (!inquiry) { - logger.error(`Inquiry for visitor ${guest._id} not found`); - throw new Error('inquiry-not-found'); - } - + '', + { _id: guest._id, username: guest.username }, + { groupable: false, token: guest.token }, + ); void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomStarted, room); - const livechatSetting = await LivechatRooms.updateRoomCount(); - if (livechatSetting) { - void notifyOnSettingChanged(livechatSetting); - } - await this.processNewInquiry(inquiry, room, defaultAgent); const newRoom = await LivechatRooms.findOneById(rid); @@ -387,6 +421,7 @@ export class QueueManager { guest, message: message?.msg, extraData: { source }, + roomCreated: false, }); if (!inquiry) { throw new Error('inquiry-not-found'); diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 4c575d02e10f0..92064ee42fdaa 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -9,7 +9,17 @@ import type { AtLeast, ILivechatContact, } from '@rocket.chat/core-typings'; -import type { FindCursor, UpdateResult, AggregationCursor, Document, FindOptions, DeleteResult, Filter, UpdateOptions } from 'mongodb'; +import type { + FindCursor, + UpdateResult, + AggregationCursor, + Document, + FindOptions, + DeleteResult, + Filter, + UpdateOptions, + ClientSession, +} from 'mongodb'; import type { FindPaginated } from '..'; import type { Updater } from '../updater'; @@ -179,7 +189,7 @@ export interface ILivechatRoomsModel extends IBaseModel { updateEmailThreadByRoomId(roomId: string, threadIds: string[] | string): Promise; findOneLastServedAndClosedByVisitorToken(visitorToken: string, options?: FindOptions): Promise; findOneByVisitorToken(visitorToken: string, fields?: FindOptions['projection']): Promise; - updateRoomCount(): Promise; + updateRoomCount(session?: ClientSession): Promise; findOpenByVisitorToken( visitorToken: string, options?: FindOptions, diff --git a/packages/models/src/models/LivechatRooms.ts b/packages/models/src/models/LivechatRooms.ts index ea3124200afc6..3a1b33f612520 100644 --- a/packages/models/src/models/LivechatRooms.ts +++ b/packages/models/src/models/LivechatRooms.ts @@ -28,6 +28,7 @@ import type { UpdateResult, AggregationCursor, UpdateOptions, + ClientSession, } from 'mongodb'; import { Settings } from '../index'; @@ -1907,8 +1908,8 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.findOne(query, options); } - async updateRoomCount() { - return Settings.incrementValueById('Livechat_Room_Count', 1, { returnDocument: 'after' }); + async updateRoomCount(session?: ClientSession) { + return Settings.incrementValueById('Livechat_Room_Count', 1, { returnDocument: 'after', session }); } findOpenByVisitorToken(visitorToken: string, options: FindOptions = {}, extraQuery: Filter = {}) { From 04bab55996b3a53a34b5053eef8336a38516440d Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 17 Jan 2025 08:55:15 -0600 Subject: [PATCH 2/7] Ts lint --- apps/meteor/app/livechat/server/lib/Helper.ts | 4 ++-- apps/meteor/app/livechat/server/lib/QueueManager.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 74204769661ab..f616dc9f3d878 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -28,13 +28,13 @@ import { LivechatRooms, LivechatDepartment, Subscriptions, - Rooms, Users, LivechatContacts, } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { ClientSession, ObjectId } from 'mongodb'; +import type { ClientSession } from 'mongodb'; +import { ObjectId } from 'mongodb'; import { Livechat as LivechatTyped } from './LivechatTyped'; import { queueInquiry, saveQueueInquiry } from './QueueManager'; diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 83cdbde098c79..fc39475825394 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -12,6 +12,7 @@ import type { } from '@rocket.chat/core-typings'; import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; +import type { InsertionModel } from '@rocket.chat/model-typings'; import { LivechatContacts, LivechatDepartment, LivechatDepartmentAgents, LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { Match, check } from 'meteor/check'; @@ -25,6 +26,7 @@ import { getOnlineAgents } from './getOnlineAgents'; import { getInquirySortMechanismSetting } from './settings'; import { dispatchInquiryPosition } from '../../../../ee/app/livechat-enterprise/server/lib/Helper'; import { callbacks } from '../../../../lib/callbacks'; +import { client, shouldRetryTransaction } from '../../../../server/database/utils'; import { sendNotification } from '../../../lib/server'; import { notifyOnLivechatInquiryChangedById, @@ -34,8 +36,6 @@ import { import { settings } from '../../../settings/server'; import { i18n } from '../../../utils/lib/i18n'; import { getOmniChatSortQuery } from '../../lib/inquiries'; -import { InsertionModel } from '@rocket.chat/model-typings'; -import { client, shouldRetryTransaction } from '/server/database/utils'; const logger = new Logger('QueueManager'); @@ -421,7 +421,6 @@ export class QueueManager { guest, message: message?.msg, extraData: { source }, - roomCreated: false, }); if (!inquiry) { throw new Error('inquiry-not-found'); From db76fd75d3fbd08718a7ed6a6ca3c92865b07d90 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 17 Jan 2025 10:03:02 -0600 Subject: [PATCH 3/7] Create many-badgers-jam.md --- .changeset/many-badgers-jam.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/many-badgers-jam.md diff --git a/.changeset/many-badgers-jam.md b/.changeset/many-badgers-jam.md new file mode 100644 index 0000000000000..cdc800ed1aaeb --- /dev/null +++ b/.changeset/many-badgers-jam.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/model-typings": minor +"@rocket.chat/models": minor +--- + +Makes Omnichannel converstion start process transactional. From 827e75bf3a55ce93f9f67f7d71b7b8d8a3a1b9a6 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 17 Jan 2025 12:47:29 -0600 Subject: [PATCH 4/7] remove setting notifier --- apps/meteor/app/livechat/server/lib/QueueManager.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index fc39475825394..96717d44261ad 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -28,11 +28,7 @@ import { dispatchInquiryPosition } from '../../../../ee/app/livechat-enterprise/ import { callbacks } from '../../../../lib/callbacks'; import { client, shouldRetryTransaction } from '../../../../server/database/utils'; import { sendNotification } from '../../../lib/server'; -import { - notifyOnLivechatInquiryChangedById, - notifyOnLivechatInquiryChanged, - notifyOnSettingChanged, -} from '../../../lib/server/lib/notifyListener'; +import { notifyOnLivechatInquiryChangedById, notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { i18n } from '../../../utils/lib/i18n'; import { getOmniChatSortQuery } from '../../lib/inquiries'; @@ -239,10 +235,8 @@ export class QueueManager { extraData: { ...extraData, source: roomInfo.source }, session, }); - const livechatSetting = await LivechatRooms.updateRoomCount(session); - if (livechatSetting) { - void notifyOnSettingChanged(livechatSetting); - } + // TODO: investigate if this setting is actually useful somewhere + await LivechatRooms.updateRoomCount(session); await session.commitTransaction(); return { room, inquiry }; } catch (e) { From 3396c14f1622d91b7c2ce611231b5e8ee54550f4 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 20 Jan 2025 07:23:13 -0600 Subject: [PATCH 5/7] remove settin from transaction --- apps/meteor/app/livechat/server/lib/QueueManager.ts | 4 ++-- packages/model-typings/src/models/ILivechatRoomsModel.ts | 2 +- packages/models/src/models/LivechatRooms.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 96717d44261ad..f9c05de828a3f 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -235,8 +235,6 @@ export class QueueManager { extraData: { ...extraData, source: roomInfo.source }, session, }); - // TODO: investigate if this setting is actually useful somewhere - await LivechatRooms.updateRoomCount(session); await session.commitTransaction(); return { room, inquiry }; } catch (e) { @@ -330,6 +328,8 @@ export class QueueManager { // All the actions that happened inside createLivechatRoom are now outside this transaction const { room, inquiry } = await this.startConversation(rid, insertionRoom, guest, roomInfo, defaultAgent, message, extraData); + // TODO: investigate if this setting is actually useful somewhere + await LivechatRooms.updateRoomCount(); await callbacks.run('livechat.newRoom', room); await Message.saveSystemMessageAndNotifyUser( 'livechat-started', diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 92064ee42fdaa..242d3ae125625 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -189,7 +189,7 @@ export interface ILivechatRoomsModel extends IBaseModel { updateEmailThreadByRoomId(roomId: string, threadIds: string[] | string): Promise; findOneLastServedAndClosedByVisitorToken(visitorToken: string, options?: FindOptions): Promise; findOneByVisitorToken(visitorToken: string, fields?: FindOptions['projection']): Promise; - updateRoomCount(session?: ClientSession): Promise; + updateRoomCount(): Promise; findOpenByVisitorToken( visitorToken: string, options?: FindOptions, diff --git a/packages/models/src/models/LivechatRooms.ts b/packages/models/src/models/LivechatRooms.ts index 3a1b33f612520..0b30a58a00cef 100644 --- a/packages/models/src/models/LivechatRooms.ts +++ b/packages/models/src/models/LivechatRooms.ts @@ -1908,8 +1908,8 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.findOne(query, options); } - async updateRoomCount(session?: ClientSession) { - return Settings.incrementValueById('Livechat_Room_Count', 1, { returnDocument: 'after', session }); + async updateRoomCount() { + return Settings.incrementValueById('Livechat_Room_Count', 1, { returnDocument: 'after' }); } findOpenByVisitorToken(visitorToken: string, options: FindOptions = {}, extraQuery: Filter = {}) { From ceea6860b9907dce70c722014ec28f14ca3088af Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 20 Jan 2025 07:31:40 -0600 Subject: [PATCH 6/7] lint --- packages/models/src/models/LivechatRooms.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/models/src/models/LivechatRooms.ts b/packages/models/src/models/LivechatRooms.ts index 0b30a58a00cef..ea3124200afc6 100644 --- a/packages/models/src/models/LivechatRooms.ts +++ b/packages/models/src/models/LivechatRooms.ts @@ -28,7 +28,6 @@ import type { UpdateResult, AggregationCursor, UpdateOptions, - ClientSession, } from 'mongodb'; import { Settings } from '../index'; From b9e90bdfde618adca49147926e00e5bab9ca57e5 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 20 Jan 2025 07:32:23 -0600 Subject: [PATCH 7/7] lint --- .../model-typings/src/models/ILivechatRoomsModel.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 242d3ae125625..4c575d02e10f0 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -9,17 +9,7 @@ import type { AtLeast, ILivechatContact, } from '@rocket.chat/core-typings'; -import type { - FindCursor, - UpdateResult, - AggregationCursor, - Document, - FindOptions, - DeleteResult, - Filter, - UpdateOptions, - ClientSession, -} from 'mongodb'; +import type { FindCursor, UpdateResult, AggregationCursor, Document, FindOptions, DeleteResult, Filter, UpdateOptions } from 'mongodb'; import type { FindPaginated } from '..'; import type { Updater } from '../updater';