Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] #345 - where smarti conversation cache got out of synch #346

Merged
merged 37 commits into from
Jun 14, 2018

Conversation

ruKurz
Copy link

@ruKurz ruKurz commented Jun 5, 2018

Closes #345

This PR includes:

  • force re-synch all rooms and messages with smarti when re-synch is triggered via admin UI
  • updating the livechat-external-message collection when replacing/creating new convIds in smarti
  • reducing redundant code for getting conversations

@ghost ghost assigned ruKurz Jun 5, 2018
@ghost ghost added the progress:working label Jun 5, 2018
@ruKurz ruKurz requested review from ThomasRoehl and mrsimpson June 5, 2018 23:00
Copy link
Member

@mrsimpson mrsimpson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change is comparatively huge - for actually two places in which functionality has changed. At least this is how far I understood it. This raised a lot of additional questions which I would like to get clarified prior to merging this. Anyway: Thanks for analyzing it 👍

* @param smartiConversationId
* @param token
*/
static analysisCompleted(roomId, smartiConversationId, token) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you think there should be no interface method anymore for Smarti to inform about a completed analysis?
As written above, I'd favor to keep the adapter as rich as possible. However you are right: The code for updating the LivechatExternalMessage was redundant: _updateMapping should be called

@@ -27,7 +27,8 @@ RocketChat.API.v1.addRoute('smarti.result/:_token', {authRequired: false}, {
if (this.urlParams._token && this.urlParams._token === rcWebhookToken) {

SystemLogger.debug('Smarti - got conversation result:', JSON.stringify(this.bodyParams, null, 2));
SmartiAdapter.analysisCompleted(this.bodyParams.channelId, this.bodyParams.conversationId, this.bodyParams.token);
SmartiAdapter.updateMapping(this.bodyParams.channelId, this.bodyParams.conversationId, new Date(), this.bodyParams.token);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This updating information retrieved from Smarti and notifying about it was intended to be handled in the semantically richer analysisCompleted Method. I tried to have no application code in the routes but instead have the routes only handle the tech and then pass on the relevant data to the adapter layer. Reverting from this should be well justified.

@@ -35,34 +36,47 @@ export class SmartiAdapter {
}

/**
* Returns a Smarti conversation Id for the given roomId.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty much what the signature is saying, isn't it? I intentionally avoided redundant information in comments and obviously had nothing more to say about it ;)

@@ -18,7 +19,7 @@ export class SmartiAdapter {
return RocketChat.settings.get('Assistify_AI_Smarti_Domain');
}

static _updateMapping(roomId, conversationId, timestamp) {
static updateMapping(roomId, conversationId, timestamp) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having the private methods prefixed _ was intentional. Changing the scope of a method is kind-of a huge change which shouldn't be done imho, since this is only a helper method which is not subject to anyone outside the adapter.

@@ -99,7 +113,7 @@ export class SmartiAdapter {
SystemLogger.debug('RocketChatMessage:', message);
SystemLogger.debug('Message:', requestBodyMessage);

let conversationId = SmartiAdapter._getConversationId(message.rid, message);
let conversationId = SmartiAdapter.getConversationId(message.rid);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems as if there has been a consumer who was passing on the message. Thus, again: Why remove this from the mapping? Providing the message allos a more precise timestamping.
And the semantic is well included in the signature:
"get the current view Smarti has about a conversation for a given message which has occurred". Without the message, to me it looks like we could also get an outdated state.

if (smartiResponse && smartiResponse.conversationId) {
conversationId = smartiResponse.conversationId;
SystemLogger.debug(`Retrieving conversation ID for channel: ${ roomId }`);
const m = RocketChat.models.LivechatExternalMessage.findOneById(roomId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason you renamed smartiResponse to m? Do you think it was not explanatory earlier on? The collection holds the Smarti-Analysis and should better be named like that, but I was reluctant to perform this change yet.

// 404 is expected if no mapping exists
if (error.response.statusCode === 404) {
return null;
const conversation = RocketChat.RateLimiter.limitFunction(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason you limited this rate?

conversationId = conversation.id;
const timestamp = message ? message.ts : Date.now();
SmartiAdapter._updateMapping(roomId, conversationId, timestamp);
let timestamp = conversation.messages &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Essentially, this is the actual change, right? Instead of utilizing the timestamp from the message, you take the timestamp from the conversation stored in Smarti. How can they differ? I'd love to read some comment about the "why we do it like that" in the code


Meteor.methods({
triggerFullResync() {
SystemLogger.info('Full Smarti resync triggered');

const query = {$or: [{outOfSync: true}, {outOfSync: {$exists: false}}]};
const query = {$or: [{outOfSync: true}, {outOfSync: false}, {outOfSync: {$exists: false}}]};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now this includes pretty much anything, doesn't it? a boolean value can be absent or have the values true or false. Why did you introduce this? Should we not - given the method name - just remove the selection criterion?

SmartiProxy.propagateToSmarti(verbs.post, 'conversation', conversation);
const response = SmartiProxy.propagateToSmarti(verbs.post, 'conversation', conversation);
SystemLogger.debug('Update conversation with id: ', response.id);
SmartiAdapter.updateMapping(rid, response.id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this should be done implicitly inside the Smarti adapter. Actually, I would expect we don't use the SmartiProxy outside the Smarti Adapter. I obviously oversaw this earlier on.

Added an admin option to re-synch completely.
@ruKurz ruKurz changed the title [FIX] #345 - where outdated conversation ids could not be found in smarti [FIX] #345 - where the smarti conversation cache got out of synch Jun 7, 2018
@ruKurz ruKurz changed the title [FIX] #345 - where the smarti conversation cache got out of synch [FIX] #345 - where smarti conversation cache got out of synch Jun 7, 2018
@ruKurz
Copy link
Author

ruKurz commented Jun 7, 2018

I've now finished my work so far.

The current status should ensure that the cache for the Smarti analysis results is updated correctly. The most important concepts (visibility of methods and the distribution of responsibilities) were taken into account. In the admin backend there is now an additional option to perform a complete synchronization with Smarti.

@ruKurz
Copy link
Author

ruKurz commented Jun 7, 2018

@mrsimpson Thank you for the valuable comments from the review. It would be great if you could take a look at the current state. The most important changes I've made you'll find within the SmartiAdapter.

Copy link
Member

@mrsimpson mrsimpson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks way better now 👍 . The new determination of the sync-timestamp raises some questions as well as the limiting.

@@ -27,7 +27,8 @@ RocketChat.API.v1.addRoute('smarti.result/:_token', {authRequired: false}, {
if (this.urlParams._token && this.urlParams._token === rcWebhookToken) {

SystemLogger.debug('Smarti - got conversation result:', JSON.stringify(this.bodyParams, null, 2));
SmartiAdapter.analysisCompleted(this.bodyParams.channelId, this.bodyParams.conversationId, this.bodyParams.token);
SmartiAdapter.analysisCompleted(this.bodyParams.channelId, this.bodyParams.conversationId, Date.now(), this.bodyParams.token);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ruKurz after y'day's decision to use the timestamp of the smarti conversation for the mapping information, why do we pass Date.now() into this method actually?

*/
static analysisCompleted(roomId, smartiConversationId, token) {
static analysisCompleted(roomId, conversationId, timestamp, token) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not like that we've always got the conversation at hand once this method is called? Then, we could (and should) get the timestamp via _getLastConversationAnalyzedTime and remove the conversationId and timestamp from the signature.

return false;

conversation = RocketChat.RateLimiter.limitFunction(
SmartiProxy.propagateToSmarti, 5, 1000, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should limit the rate of this method being called - but not the interactions inside this method with Smarti - it would slow-down the sync.

SmartiProxy.propagateToSmarti(verbs.delete,
`conversation/${ conversation.id }`, null);
RocketChat.RateLimiter.limitFunction(
SmartiProxy.propagateToSmarti, 5, 1000, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here: no limiting of the propagation

@@ -137,7 +154,16 @@ Meteor.methods({
conversation.messages.push(newMessage);
}

SmartiProxy.propagateToSmarti(verbs.post, 'conversation', conversation);
const analyzedConversation = RocketChat.RateLimiter.limitFunction(
SmartiProxy.propagateToSmarti, 5, 1000, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and again: limiting

@mrsimpson
Copy link
Member

P.s.: If you merge develop into this PR now, a docker image of the PR will be built automatically - at least once you fixed the linting ;)

@ruKurz
Copy link
Author

ruKurz commented Jun 8, 2018

Hi @mrsimpson, I had to notice that the asynchronous callbacks have been replaced by using the synchronous analysis when updating to Smarti 0.7.0. Thought I'd just put it back in. I had to realize that the response body from Smarti, which is sent to the RC Webhook, only contains the analysis results but not the roomId, so it is not possible to notify the room at all.

If we want to get through the current changes, we have to do the analysis synchronously or we can use the conversationId cache (livechat-external-messages) in order to get the roomId by conversationId on the Assistify.Chat side.

For now I've implemented to get the roomId by the livechat-external-messages when the asynchronous callback gets back to Assistify.Chat. This resolves the dependencies to Smarti for now and we can make use of asynchronous communication between Smarti an Assistify.Chat.

Created an Issue for Smarti: redlink-gmbh/smarti#251

Copy link
Member

@mrsimpson mrsimpson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some intermediate comments

SmartiProxy.propagateToSmarti(verbs.delete, `conversation/${ conversationId }/message/${ message._id }`);
SmartiAdapter._getAnalysisResult(message.rid, conversationId);
Copy link
Member

@mrsimpson mrsimpson Jun 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why's an analysis result retrieved after delete?

// post the new conversation to Smarti
const newSmartiConversation = SmartiProxy.propagateToSmarti(verbs.post, 'conversation', conversation);
// get the analysis result
const analysisResult = SmartiProxy.propagateToSmarti(verbs.get, `conversation/${ newSmartiConversation.id }/analysis`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When submitting analysis: true in the body of the post, the analysis is synchronously returned. Thus, the get can be omitted.

Copy link
Member

@mrsimpson mrsimpson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks reasonable to me ;)

@@ -29,6 +31,9 @@ export class CreateRequestFromExpertise extends CreateRequestBase {
this._members = CreateRequestFromExpertise.getExperts(this._expertise);
}
const roomCreateResult = RocketChat.createRoom('r', this.name, Meteor.user() && Meteor.user().username, this._members, false, {expertise: this._expertise});
const knowledgeAdapter = getKnowledgeAdapter();
knowledgeAdapter.afterCreateChannel(roomCreateResult.rid);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked myself "Why not have this registered as a hook?". Then I found the answer: there's no good implementation of this in the core: There's only

if (type === 'c') {
		Meteor.defer(() => {
			RocketChat.callbacks.run('afterCreateChannel', owner, room);
		});
	} else if (type === 'p') {
		Meteor.defer(() => {
			RocketChat.callbacks.run('afterCreatePrivateGroup', owner, room);
		});
	}

in createRoom.js

@ruKurz
Copy link
Author

ruKurz commented Jun 13, 2018

From my side this PR has an nearly good state.

Include

  • SmartiAdapter.onMessage should trigger a resync to heal outdated conversations, when at least one message is out of sync (include)
  • SmartiAdapter.resync should handle livechat rooms as well (include)
  • previously implemented end-to-end tests do not work because of Rocket.Chat core changes, make them run again (include)

create a separate Issue

Noticed

@ThomasRoehl
Copy link

Created issues:
#350
#351

@ruKurz
Copy link
Author

ruKurz commented Jun 13, 2018

@ThomasRoehl did you make the end-to-end tests run again?

@ruKurz
Copy link
Author

ruKurz commented Jun 14, 2018

The last commit 5644649 fixes #350

@ThomasRoehl
Copy link

Tests are finally running through on my machine, hopefully on circleCi as well.

@ruKurz
Copy link
Author

ruKurz commented Jun 14, 2018

tests in circleCI went well. So is there anything keeping us from merging this PR into develop?
@mrsimpson could you please accept the requested changes / finish your review?

@ruKurz ruKurz merged commit 41fb6d4 into develop Jun 14, 2018
@ghost ghost removed the progress:working label Jun 14, 2018
@mrsimpson mrsimpson deleted the fix/smarti-results-not-loaded branch December 7, 2020 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

The knowledge base is not displayed in many topics and requests
3 participants