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 race condition between bot and user activity #3705

Merged
merged 7 commits into from
Feb 5, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Fixes [#3565](https://github.com/microsoft/BotFramework-WebChat/issues/3565). Allow strikethrough `<s>` on sanitize markdown, by [@corinagum](https://github.com/corinagum) in PR [#3646](https://github.com/microsoft/BotFramework-WebChat/pull/3646)
- Fixes [#3672](https://github.com/microsoft/BotFramework-WebChat/issues/3672). Center the icon of send box buttons vertically and horizontally, by [@compulim](https://github.com/compulim) in PR [#3673](https://github.com/microsoft/BotFramework-WebChat/pull/3673)
- Fixes [#3683](https://github.com/microsoft/BotFramework-WebChat/issues/3683). Activities should be acknowledged when user scrolls to bottom, by [@compulim](https://github.com/compulim) in PR [#3684](https://github.com/microsoft/BotFramework-WebChat/pull/3684)
- Fixes [#3431](https://github.com/microsoft/BotFramework-WebChat/issues/3431). Race condition between the first bot activity and first user activity, which should not cause the first bot activity to be delayed, by [@compulim](https://github.com/compulim) in PR [#3705](https://github.com/microsoft/BotFramework-WebChat/pull/3705)

### Changed

Expand Down
113 changes: 113 additions & 0 deletions __tests__/html/accessibility.delayActivity.raceCondition.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<script crossorigin="anonymous" src="/__dist__/testharness.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
</head>
<body>
<div id="webchat"></div>
<script type="text/babel" data-presets="env,stage-3,react">
const {
lolex,
WebChatTest: {
conditions,
createDirectLineWithTranscript,
createStore,
host,
Observable,
pageObjects,
timeouts,
token
}
} = window;

const clock = (window.clock = lolex.install());

// This test is to make sure we don't regress issue #3431, a race condition:

// - After created a conversation
// - User send an activity (or Web Chat programmatically sending out an activity)
// - At the same time, the bot also send another activity, with reply to ID of a bogus activity

// The timeout occurs if the user's message was send before the bot message was received.

// Originally, we allow the bot's activity to show up even they point to a bogus activity, if that message was the very first activity in the transcript.

// Because the user's activity was already in the transcript, the relaxation was not granted. Thus, the bot's activity is subject to "reply to ID" and a timeout.

(async function () {
const directLine = createDirectLineWithTranscript([], {
overridePostActivity: activity => {
return new Observable(({ complete, next }) => {
const id = '00001';

directLine.activityDeferredObservable.next({
channelData: activity.channelData,
from: {
id: 'user',
role: 'user'
},
id,
text: 'Aloha, bot!',
timestamp: new Date(Date.now() + 1).toISOString(),
type: 'message'
});

next(id);
complete();
});
}
});

const store = createStore({}, ({ dispatch }) => next => action => {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
dispatch({
payload: {
text: 'Aloha, bot!'
},
type: 'WEB_CHAT/SEND_MESSAGE'
});
}

return next(action);
});

window.WebChat.renderWebChat(
{
directLine,
store
},
document.getElementById('webchat')
);

await pageObjects.wait(conditions.webChatRendered(), timeouts.ui);

// Wait for "Connecting..." message to dismiss
clock.tick(600);

await pageObjects.wait(conditions.uiConnected(), timeouts.directLine);

directLine.activityDeferredObservable.next({
from: {
id: 'bot',
role: 'bot'
},
id: '00000',
replyToId: 'ABCDE',
text: 'Hello, John!',
timestamp: new Date(Date.now() + 0).toISOString(),
type: 'message'
});

// The bot's activity 00000 will show up immediately because it is the very first bot activity and should not send to "Reply-to-ID" queue.
await pageObjects.wait(conditions.numActivitiesShown(2), timeouts.directLine);

await host.done();
})().catch(async err => {
console.error(err);

await host.error(err);
});
</script>
</body>
</html>
8 changes: 8 additions & 0 deletions __tests__/html/accessibility.delayActivity.raceCondition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @jest-environment ./__tests__/html/__jest__/WebChatEnvironment.js
*/

describe('accessibility requirement', () => {
test('race conditions between first bot activity and first user activity should not cause any delay to the first bot activity', () =>
runHTMLTest('accessibility.delayActivity.raceCondition.html'));
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
WebChatTest: { conditions, createStore, host, pageObjects, timeouts }
} = window;

(async function() {
(async function () {
const clock = lolex.install();
const directLine = createDirectLineForTest({ withReplyToId: true });

Expand All @@ -30,6 +30,9 @@
// Wait for "Connecting..." message to dismiss
clock.tick(600);

// TODO: [P1] #3707 This line should be removed when the SDK removed the bogus replyToId.
compulim marked this conversation as resolved.
Show resolved Hide resolved
directLine.botSendMessage({ replyToId: 'bogus', type: 'event' });

await pageObjects.sendMessageViaSendBox('Hello, World!', { waitForSend: false });

// This screenshot should show only user-initiated message "Hello, World!" with "Sending" status.
Expand Down
19 changes: 13 additions & 6 deletions __tests__/html/assets/accessibility.delayActivity.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,19 @@ window.TestAsset = {
activity$: shareObservable(activityDeferred$.observable),
connectionStatus$: shareObservable(connectionStatusDeferred$.observable),
numActivities: 0,
botSendMessage: activity => {
activityDeferred$.next({
from: {
role: 'bot'
},
id: Math.random().toString(36).substr(2, 10),
timestamp: new Date().toISOString(),
type: 'message',
...activity
});
},
postActivity: activity => {
const id = Math.random()
.toString(36)
.substr(2, 10);
const id = Math.random().toString(36).substr(2, 10);
const now = Date.now();
const timestamp = new Date(now).toISOString();
const postActivityDeferred$ = createDeferredObservable();
Expand All @@ -31,9 +40,7 @@ window.TestAsset = {
from: {
role: 'bot'
},
id: Math.random()
.toString(36)
.substr(2, 10),
id: Math.random().toString(36).substr(2, 10),
text: `You said: ${activity.text}`,
timestamp: new Date(now + 1).toISOString(),
type: 'message',
Expand Down
15 changes: 11 additions & 4 deletions packages/core/src/sagas/queueIncomingActivitySaga.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ function* waitForActivityId(replyToId, initialActivities) {
break;
}

yield take(INCOMING_ACTIVITY);
const {
payload: { activity }
} = yield take(INCOMING_ACTIVITY);

if (activity.id === replyToId) {
break;
}

activities = yield select(activitiesSelector);
}
Expand All @@ -48,14 +54,15 @@ function* queueIncomingActivity({ userID }) {
{ payload: { activity } },
initialActivities
) {
// This is for accessibility issue.
// This is for resolving an accessibility issue.
compulim marked this conversation as resolved.
Show resolved Hide resolved
// If the incoming activity has "replyToId" field, hold on it until the activity replied to is in the transcript, then release this one.
const { replyToId } = activity;
const initialBotActivities = initialActivities.filter(({ from: { role } }) => role === 'bot');

// To speed up the first activity render time, we do not delay the first activity.
// To speed up the first activity render time, we do not delay the first activity from the bot.
// Even if it is the first activity from the bot, the bot might be "replying" to the "conversationUpdate" event.
// Thus, the "replyToId" will always be there even it is the first activity in the conversation.
if (replyToId && initialActivities.length) {
if (replyToId && initialBotActivities.length) {
// Either the activity replied to is in the transcript or after timeout.
const result = yield race({
_: waitForActivityId(replyToId, initialActivities),
Expand Down
2 changes: 2 additions & 0 deletions packages/testharness/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import classNames from 'classnames';
import createDeferred from 'p-defer-es5';
import expect from 'expect';
import lolex from 'lolex';
import Observable from 'core-js/features/observable';
corinagum marked this conversation as resolved.
Show resolved Hide resolved
import updateIn from 'simple-update-in';

import { EventIterator } from './external/event-iterator';
Expand Down Expand Up @@ -174,6 +175,7 @@ export {
jobs,
loadTranscriptAsset,
MockAudioContext,
Observable,
pageObjects,
parseURLParams,
pcmWaveArrayBufferToRiffWaveArrayBuffer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function updateRelativeTimestamp(now, activity) {
};
}

export default function createDirectLineWithTranscript(activitiesOrFilename) {
export default function createDirectLineWithTranscript(activitiesOrFilename, { overridePostActivity } = {}) {
const now = Date.now();
const patchActivity = updateRelativeTimestamp.bind(null, now);
const connectionStatusDeferredObservable = createDeferredObservable(() => {
Expand Down Expand Up @@ -61,6 +61,10 @@ export default function createDirectLineWithTranscript(activitiesOrFilename) {
connectionStatusDeferredObservable,
end: () => {},
postActivity: activity => {
if (overridePostActivity) {
return overridePostActivity(activity);
}

const id = Math.random().toString(36).substr(2, 5);

activityDeferredObservable.next(
Expand Down