Merge pull request #3628 from LLK/revert-3618-hotfix/join-retry-session

Revert "[Develop] Hotfix/join retry session"
This commit is contained in:
Benjamin Wheeler 2020-01-09 08:47:52 -05:00 committed by GitHub
commit 7c9a098598
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 63 additions and 303 deletions

View file

@ -121,16 +121,13 @@ class JoinFlow extends React.Component {
// * "birth_month": ["Ensure this value is greater than or equal to 1."] // * "birth_month": ["Ensure this value is greater than or equal to 1."]
handleRegistrationResponse (err, body, res) { handleRegistrationResponse (err, body, res) {
this.setState({ this.setState({
numAttempts: this.state.numAttempts + 1 numAttempts: this.state.numAttempts + 1,
waiting: false
}, () => { }, () => {
const success = this.registrationIsSuccessful(err, body, res); const success = this.registrationIsSuccessful(err, body, res);
if (success) { if (success) {
this.props.refreshSessionWithRetry().then(() => { this.props.refreshSession();
this.setState({ this.setState({step: this.state.step + 1});
step: this.state.step + 1,
waiting: false
});
});
return; return;
} }
// now we know something went wrong -- either an actual error (client-side // now we know something went wrong -- either an actual error (client-side
@ -138,10 +135,7 @@ class JoinFlow extends React.Component {
// if an actual error, prompt user to try again. // if an actual error, prompt user to try again.
if (err || res.statusCode !== 200) { if (err || res.statusCode !== 200) {
this.setState({ this.setState({registrationError: {errorAllowsTryAgain: true}});
registrationError: {errorAllowsTryAgain: true},
waiting: false
});
return; return;
} }
@ -170,8 +164,7 @@ class JoinFlow extends React.Component {
registrationError: { registrationError: {
errorAllowsTryAgain: false, errorAllowsTryAgain: false,
errorMsg: errorMsg errorMsg: errorMsg
}, }
waiting: false
}); });
}); });
} }
@ -290,15 +283,15 @@ JoinFlow.propTypes = {
createProjectOnComplete: PropTypes.bool, createProjectOnComplete: PropTypes.bool,
intl: intlShape, intl: intlShape,
onCompleteRegistration: PropTypes.func, onCompleteRegistration: PropTypes.func,
refreshSessionWithRetry: PropTypes.func refreshSession: PropTypes.func
}; };
const IntlJoinFlow = injectIntl(JoinFlow); const IntlJoinFlow = injectIntl(JoinFlow);
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
refreshSessionWithRetry: () => ( refreshSession: () => {
dispatch(sessionActions.refreshSessionWithRetry()) dispatch(sessionActions.refreshSession());
) }
}); });
// Allow incoming props to override redux-provided props. Used to mock in tests. // Allow incoming props to override redux-provided props. Used to mock in tests.

View file

@ -1,69 +0,0 @@
const api = require('./api');
module.exports = {};
/*
requestSessionWithRetry()
Retries the session api call until it has either reached the limit of number of
retries, or received a successful response with that contains a 'user' field in
its body.
Each time it retries, it will double its previous waiting time, and subtract
that time from the totalDelayMS parameter.
example of what this might look like:
1st call:
receives params: retriesLeft=3 and totalDelayMS=3500
performs api call
delay until next call: 3500 / (2^3 - 1) = 500ms
next call params: retriesLeft=2, totalDelayMS=3000
2nd call:
receives params: retriesLeft=2 and totalDelayMS=3000
performs api call
delay until next call: 3000 / (2^2 - 1) = 1000ms
next call: retriesLeft=1, totalDelayMS=2000
3rd call:
receives params: retriesLeft=1, totalDelayMS=2000
performs api call
delay until next call: 2000 / (2^1 - 1) = 2000ms
next call: retriesLeft=0, totalDelayMS=0
4th call:
receives params: retriesLeft=0, totalDelayMS=0
performs api call
returns the response, even if it is undefined or empty
total api calls: 4
total delay time: 3500ms
*/
module.exports.requestSessionWithRetry = (resolve, reject, retriesLeft, totalDelayMS) => {
api({
host: '',
uri: '/session/'
}, (err, body, response) => {
if (err || (response && response.statusCode === 404)) {
return reject(err);
}
if (typeof body === 'undefined' || body === null || !body.user) {
if (retriesLeft < 1) {
return resolve(body);
}
const nextTimeout = totalDelayMS / (Math.pow(2, retriesLeft) - 1);
return setTimeout(
module.exports.requestSessionWithRetry.bind(
null, resolve, reject, retriesLeft - 1, totalDelayMS - nextTimeout
),
nextTimeout
);
}
return resolve(body);
});
};
module.exports.requestSession = (resolve, reject) => (
module.exports.requestSessionWithRetry(resolve, reject, 0, 0)
);

View file

@ -110,9 +110,8 @@ module.exports.handleCompleteRegistration = createProject => (dispatch => {
// to be logged in before we try creating a project due to replication lag. // to be logged in before we try creating a project due to replication lag.
window.location = '/'; window.location = '/';
} else { } else {
dispatch(sessionActions.refreshSessionWithRetry()).then( dispatch(sessionActions.refreshSession());
dispatch(module.exports.setRegistrationOpen(false)) dispatch(module.exports.setRegistrationOpen(false));
);
} }
}); });

View file

@ -1,7 +1,7 @@
const keyMirror = require('keymirror'); const keyMirror = require('keymirror');
const defaults = require('lodash.defaults'); const defaults = require('lodash.defaults');
const {requestSession, requestSessionWithRetry} = require('../lib/session'); const api = require('../lib/api');
const messageCountActions = require('./message-count.js'); const messageCountActions = require('./message-count.js');
const permissionsActions = require('./permissions.js'); const permissionsActions = require('./permissions.js');
@ -61,7 +61,13 @@ module.exports.setStatus = status => ({
status: status status: status
}); });
const handleSessionResponse = (dispatch, body) => { module.exports.refreshSession = () => (dispatch => {
dispatch(module.exports.setStatus(module.exports.Status.FETCHING));
api({
host: '',
uri: '/session/'
}, (err, body) => {
if (err) return dispatch(module.exports.setSessionError(err));
if (typeof body === 'undefined') return dispatch(module.exports.setSessionError('No session content')); if (typeof body === 'undefined') return dispatch(module.exports.setSessionError('No session content'));
if ( if (
body.user && body.user &&
@ -95,26 +101,5 @@ const handleSessionResponse = (dispatch, body) => {
dispatch(messageCountActions.getCount(body.user.username)); dispatch(messageCountActions.getCount(body.user.username));
} }
return; return;
};
module.exports.refreshSession = () => (dispatch => {
dispatch(module.exports.setStatus(module.exports.Status.FETCHING));
return new Promise((resolve, reject) => (
requestSession(resolve, reject).then(body => {
handleSessionResponse(dispatch, body);
}, err => {
dispatch(module.exports.setSessionError(err));
})
));
});
module.exports.refreshSessionWithRetry = () => (dispatch => {
dispatch(module.exports.setStatus(module.exports.Status.FETCHING));
return new Promise((resolve, reject) => (
requestSessionWithRetry(resolve, reject, 4, 7500)
)).then(body => {
handleSessionResponse(dispatch, body);
}, err => {
dispatch(module.exports.setSessionError(err));
}); });
}); });

View file

@ -41,7 +41,7 @@ describe('JoinFlow', () => {
beforeEach(() => { beforeEach(() => {
store = mockStore({sessionActions: { store = mockStore({sessionActions: {
refreshSessionWithRetry: jest.fn() refreshSession: jest.fn()
}}); }});
}); });
@ -283,31 +283,16 @@ describe('JoinFlow', () => {
expect(success).toEqual(false); expect(success).toEqual(false);
}); });
test('handleRegistrationResponse calls refreshSessionWithRetry() when passed body with success', done => { test('handleRegistrationResponse when passed body with success', () => {
const props = { const props = {
refreshSessionWithRetry: () => (new Promise(() => { // eslint-disable-line no-undef refreshSession: jest.fn()
done(); // ensures that joinFlowInstance.props.refreshSessionWithRetry() was called
}))
}; };
const joinFlowInstance = getJoinFlowWrapper(props).instance(); const joinFlowInstance = getJoinFlowWrapper(props).instance();
joinFlowInstance.handleRegistrationResponse(null, responseBodySuccess, {statusCode: 200}); joinFlowInstance.handleRegistrationResponse(null, responseBodySuccess, {statusCode: 200});
});
test('handleRegistrationResponse advances to next step when passed body with success', () => {
const props = {
refreshSessionWithRetry: () => (new Promise(resolve => { // eslint-disable-line no-undef
resolve();
}))
};
const joinFlowInstance = getJoinFlowWrapper(props).instance();
joinFlowInstance.handleRegistrationResponse(null, responseBodySuccess, {statusCode: 200});
process.nextTick(
() => {
expect(joinFlowInstance.state.registrationError).toEqual(null); expect(joinFlowInstance.state.registrationError).toEqual(null);
expect(joinFlowInstance.props.refreshSession).toHaveBeenCalled();
expect(joinFlowInstance.state.step).toEqual(1); expect(joinFlowInstance.state.step).toEqual(1);
expect(joinFlowInstance.state.waiting).toBeFalsy(); expect(joinFlowInstance.state.waiting).toBeFalsy();
}
);
}); });
test('handleRegistrationResponse when passed body with preset server error', () => { test('handleRegistrationResponse when passed body with preset server error', () => {
@ -321,7 +306,7 @@ describe('JoinFlow', () => {
test('handleRegistrationResponse with failure response, with error fields missing', () => { test('handleRegistrationResponse with failure response, with error fields missing', () => {
const props = { const props = {
refreshSessionWithRetry: jest.fn() refreshSession: jest.fn()
}; };
const joinFlowInstance = getJoinFlowWrapper(props).instance(); const joinFlowInstance = getJoinFlowWrapper(props).instance();
const responseErr = null; const responseErr = null;
@ -335,7 +320,7 @@ describe('JoinFlow', () => {
statusCode: 200 statusCode: 200
}; };
joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj); joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
expect(joinFlowInstance.props.refreshSessionWithRetry).not.toHaveBeenCalled(); expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled();
expect(joinFlowInstance.state.registrationError).toEqual({ expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: false, errorAllowsTryAgain: false,
errorMsg: null errorMsg: null
@ -354,7 +339,7 @@ describe('JoinFlow', () => {
test('handleRegistrationResponse with failure response, with no text explanation', () => { test('handleRegistrationResponse with failure response, with no text explanation', () => {
const props = { const props = {
refreshSessionWithRetry: jest.fn() refreshSession: jest.fn()
}; };
const joinFlowInstance = getJoinFlowWrapper(props).instance(); const joinFlowInstance = getJoinFlowWrapper(props).instance();
const responseErr = null; const responseErr = null;
@ -367,7 +352,7 @@ describe('JoinFlow', () => {
statusCode: 200 statusCode: 200
}; };
joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj); joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
expect(joinFlowInstance.props.refreshSessionWithRetry).not.toHaveBeenCalled(); expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled();
expect(joinFlowInstance.state.registrationError).toEqual({ expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: false, errorAllowsTryAgain: false,
errorMsg: null errorMsg: null
@ -384,11 +369,11 @@ describe('JoinFlow', () => {
test('handleRegistrationResponse when passed status 400', () => { test('handleRegistrationResponse when passed status 400', () => {
const props = { const props = {
refreshSessionWithRetry: jest.fn() refreshSession: jest.fn()
}; };
const joinFlowInstance = getJoinFlowWrapper(props).instance(); const joinFlowInstance = getJoinFlowWrapper(props).instance();
joinFlowInstance.handleRegistrationResponse({}, responseBodyMultipleErrs, {statusCode: 400}); joinFlowInstance.handleRegistrationResponse({}, responseBodyMultipleErrs, {statusCode: 400});
expect(joinFlowInstance.props.refreshSessionWithRetry).not.toHaveBeenCalled(); expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled();
expect(joinFlowInstance.state.registrationError).toEqual({ expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: true errorAllowsTryAgain: true
}); });

View file

@ -1,133 +0,0 @@
describe('session library', () => {
// respond to session requests with empty session object
let sessionNoUser = jest.fn((opts, callback) => {
callback(null, {}, {statusCode: 200});
});
// respond to session requests with session object that indicates
// successfully logged-in user
let sessionYesUser = jest.fn((opts, callback) => {
callback(null, {user: {username: 'test_username'}}, {statusCode: 200});
});
// respond to first two requests with empty session object; after that,
// respond with user in object
let sessionNoThenYes = jest.fn((opts, callback) => {
if (sessionNoThenYes.mock.calls.length <= 2) {
callback(null, {}, {statusCode: 200});
} else {
callback(null, {user: {username: 'test_username'}}, {statusCode: 200});
}
});
// respond to session requests with response code 404, indicating no session
// found for that user
let sessionNotFound = jest.fn((opts, callback) => {
callback(null, null, {statusCode: 404});
});
// respond to session requests with response code 503, indicating connection failure
let sessionConnectFailure = jest.fn((opts, callback) => {
callback(null, null, {statusCode: 503});
});
// by changing whichMockAPIRequest, we can simulate different api responses
let whichMockAPIRequest = null;
let mockAPIRequest = (opts, callback) => {
whichMockAPIRequest(opts, callback);
};
// mock lib/api.js, and include our mocked version in lib/session.js
jest.mock('../../../src/lib/api', () => {
return mockAPIRequest;
});
const sessionLib = require('../../../src/lib/session'); // eslint-disable-line global-require
afterEach(() => {
jest.clearAllMocks();
});
test('requestSession can call api 1 time, when session found', done => {
whichMockAPIRequest = sessionYesUser;
new Promise((resolve, reject) => { // eslint-disable-line no-undef
sessionLib.requestSession(resolve, reject);
}).then(body => {
expect(sessionYesUser).toHaveBeenCalledTimes(1);
expect(body).toEqual({user: {username: 'test_username'}});
done();
});
});
test('requestSession can call api 1 time, when session not found', done => {
whichMockAPIRequest = sessionNoUser;
new Promise((resolve, reject) => { // eslint-disable-line no-undef
sessionLib.requestSession(resolve, reject);
}).then(body => {
expect(sessionNoUser).toHaveBeenCalledTimes(1);
expect(body).toEqual({});
done();
});
});
test('requestSessionWithRetry can call api once', done => {
whichMockAPIRequest = sessionNoUser;
new Promise((resolve, reject) => { // eslint-disable-line no-undef
sessionLib.requestSessionWithRetry(resolve, reject, 0, 0);
}).then(() => {
expect(sessionNoUser).toHaveBeenCalledTimes(1);
done();
});
});
test('requestSessionWithRetry can call api multiple times', done => {
whichMockAPIRequest = sessionNoUser;
new Promise((resolve, reject) => { // eslint-disable-line no-undef
sessionLib.requestSessionWithRetry(resolve, reject, 2, 0);
}).then(() => {
expect(sessionNoUser).toHaveBeenCalledTimes(3);
done();
});
});
test('requestSessionWithRetry respects total delay time param within a reasonable tolerance', done => {
whichMockAPIRequest = sessionNoUser;
const startTime = new Date().getTime();
new Promise((resolve, reject) => { // eslint-disable-line no-undef
sessionLib.requestSessionWithRetry(resolve, reject, 2, 2500);
}).then(() => {
const endTime = new Date().getTime();
expect(endTime - startTime > 2000).toBeTruthy();
expect(endTime - startTime < 3000).toBeTruthy();
done();
});
});
test('requestSessionWithRetry will retry if no user found, then stop when user is found', done => {
whichMockAPIRequest = sessionNoThenYes;
new Promise((resolve, reject) => { // eslint-disable-line no-undef
sessionLib.requestSessionWithRetry(resolve, reject, 4, 3000);
}).then(body => {
expect(body).toEqual({user: {username: 'test_username'}});
expect(sessionNoThenYes).toHaveBeenCalledTimes(3);
done();
});
});
test('requestSessionWithRetry handles session not found as immediate error', done => {
whichMockAPIRequest = sessionNotFound;
new Promise((resolve, reject) => { // eslint-disable-line no-undef
sessionLib.requestSessionWithRetry(resolve, reject, 2, 0);
}).then(() => {}, err => {
expect(err).toBeFalsy();
done();
});
});
test('requestSessionWithRetry handles connection failure by retrying', done => {
whichMockAPIRequest = sessionConnectFailure;
new Promise((resolve, reject) => { // eslint-disable-line no-undef
sessionLib.requestSessionWithRetry(resolve, reject, 2, 0);
}).then(body => {
expect(sessionConnectFailure).toHaveBeenCalledTimes(3);
expect(body).toBeFalsy();
done();
});
});
});