-
Notifications
You must be signed in to change notification settings - Fork 87
feat(createRequestHtmlFragment): implemented circuit breaker #111
Changes from 2 commits
2f0786e
31da485
0dc99f2
1b3c07f
1060434
51542c6
e53c396
81ffcb4
9047392
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,24 +14,35 @@ | |
* permissions and limitations under the License. | ||
*/ | ||
|
||
/* global BigInt */ | ||
|
||
import url from 'url'; | ||
import { browserHistory } from '@americanexpress/one-app-router'; | ||
import { Map as iMap, fromJS } from 'immutable'; | ||
import { composeModules } from 'holocron'; | ||
import match from '../../../src/universal/utils/matchPromisified'; | ||
import breaker from '../../../src/server/utils/circuitBreaker'; | ||
|
||
import * as reactRendering from '../../../src/server/utils/reactRendering'; | ||
|
||
jest.mock('../../../src/universal/utils/matchPromisified'); | ||
|
||
jest.mock('holocron', () => ({ | ||
composeModules: jest.fn(() => 'composeModules'), | ||
getModule: () => () => 0, | ||
})); | ||
|
||
const sleep = (ms) => new Promise((res) => setTimeout(res, ms)); | ||
const renderForStringSpy = jest.spyOn(reactRendering, 'renderForString'); | ||
const renderForStaticMarkupSpy = jest.spyOn(reactRendering, 'renderForStaticMarkup'); | ||
const msToNs = (n) => n * 1e6; | ||
|
||
describe('createRequestHtmlFragment', () => { | ||
jest.spyOn(console, 'error').mockImplementation(() => {}); | ||
const realHrtime = process.hrtime; | ||
const mockHrtime = (...args) => realHrtime(...args); | ||
mockHrtime.bigint = jest.fn(); | ||
process.hrtime = mockHrtime; | ||
|
||
let req; | ||
let res; | ||
|
@@ -59,6 +70,8 @@ describe('createRequestHtmlFragment', () => { | |
}); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
breaker.close(); | ||
req = jest.fn(); | ||
req.headers = {}; | ||
|
||
|
@@ -78,6 +91,10 @@ describe('createRequestHtmlFragment', () => { | |
renderForStaticMarkupSpy.mockClear(); | ||
}); | ||
|
||
afterAll(() => { | ||
process.hrtime = realHrtime; | ||
}); | ||
|
||
it('returns a function', () => { | ||
const createRequestHtmlFragment = require( | ||
'../../../src/server/middleware/createRequestHtmlFragment' | ||
|
@@ -93,7 +110,6 @@ describe('createRequestHtmlFragment', () => { | |
const middleware = createRequestHtmlFragment({ createRoutes }); | ||
return middleware(req, res, next) | ||
.then(() => { | ||
const { composeModules } = require('holocron'); | ||
expect(composeModules).toHaveBeenCalled(); | ||
expect(composeModules.mock.calls[0][0]).toMatchSnapshot(); | ||
expect(dispatch).toHaveBeenCalledWith('composeModules'); | ||
|
@@ -229,4 +245,86 @@ describe('createRequestHtmlFragment', () => { | |
expect(next).toHaveBeenCalled(); | ||
/* eslint-enable no-console */ | ||
}); | ||
|
||
it('should open the circuit when event loop lag is > 30ms', async () => { | ||
expect.assertions(5); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this required with async test ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. checked locally, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. expect.assertions ensures we get passed the promise to all the assertions |
||
const createRequestHtmlFragment = require( | ||
'../../../src/server/middleware/createRequestHtmlFragment' | ||
).default; | ||
const middleware = createRequestHtmlFragment({ createRoutes }); | ||
process.hrtime.bigint | ||
.mockReturnValueOnce(BigInt(msToNs(100))) | ||
.mockReturnValueOnce(BigInt(msToNs(131))); | ||
await sleep(110); | ||
await middleware(req, res, next); | ||
expect(next).toHaveBeenCalled(); | ||
expect(composeModules).not.toHaveBeenCalled(); | ||
expect(renderForStringSpy).not.toHaveBeenCalled(); | ||
expect(renderForStaticMarkupSpy).not.toHaveBeenCalled(); | ||
expect(req.appHtml).toBe(''); | ||
}); | ||
|
||
it('should not open the circuit when event loop lag is < 30ms', async () => { | ||
expect.assertions(5); | ||
const createRequestHtmlFragment = require( | ||
'../../../src/server/middleware/createRequestHtmlFragment' | ||
).default; | ||
const middleware = createRequestHtmlFragment({ createRoutes }); | ||
process.hrtime.bigint | ||
.mockReturnValueOnce(BigInt(msToNs(100))) | ||
.mockReturnValueOnce(BigInt(msToNs(120))); | ||
await sleep(110); | ||
await middleware(req, res, next); | ||
expect(next).toHaveBeenCalled(); | ||
expect(composeModules).toHaveBeenCalled(); | ||
expect(renderForStringSpy).toHaveBeenCalled(); | ||
expect(renderForStaticMarkupSpy).not.toHaveBeenCalled(); | ||
expect(req.appHtml).toBe('hi'); | ||
}); | ||
|
||
it('should not open the circuit for partials', async () => { | ||
expect.assertions(5); | ||
const createRequestHtmlFragment = require( | ||
'../../../src/server/middleware/createRequestHtmlFragment' | ||
).default; | ||
const middleware = createRequestHtmlFragment({ createRoutes }); | ||
getState.mockImplementationOnce(() => fromJS({ | ||
rendering: { | ||
renderPartialOnly: true, | ||
}, | ||
})); | ||
process.hrtime.bigint | ||
.mockReturnValueOnce(BigInt(msToNs(100))) | ||
.mockReturnValueOnce(BigInt(msToNs(131))); | ||
await sleep(110); | ||
await middleware(req, res, next); | ||
expect(next).toHaveBeenCalled(); | ||
expect(composeModules).toHaveBeenCalled(); | ||
expect(renderForStringSpy).not.toHaveBeenCalled(); | ||
expect(renderForStaticMarkupSpy).toHaveBeenCalled(); | ||
expect(req.appHtml).toBe('hi'); | ||
}); | ||
|
||
it('should not open the when scripts are disabled', async () => { | ||
expect.assertions(5); | ||
const createRequestHtmlFragment = require( | ||
'../../../src/server/middleware/createRequestHtmlFragment' | ||
).default; | ||
const middleware = createRequestHtmlFragment({ createRoutes }); | ||
getState.mockImplementationOnce(() => fromJS({ | ||
rendering: { | ||
disableScripts: true, | ||
}, | ||
})); | ||
process.hrtime.bigint | ||
.mockReturnValueOnce(BigInt(msToNs(100))) | ||
.mockReturnValueOnce(BigInt(msToNs(131))); | ||
await sleep(110); | ||
await middleware(req, res, next); | ||
expect(next).toHaveBeenCalled(); | ||
expect(composeModules).toHaveBeenCalled(); | ||
expect(renderForStringSpy).not.toHaveBeenCalled(); | ||
expect(renderForStaticMarkupSpy).toHaveBeenCalled(); | ||
expect(req.appHtml).toBe('hi'); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* | ||
* Copyright 2020 American Express Travel Related Services Company, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express | ||
* or implied. See the License for the specific language governing | ||
* permissions and limitations under the License. | ||
*/ | ||
|
||
import CircuitBreaker from 'opossum'; | ||
import breaker, { | ||
setEventLoopLagThreshold, | ||
getEventLoopLagThreshold, | ||
} from '../../../src/server/utils/circuitBreaker'; | ||
|
||
describe('Circuit breaker', () => { | ||
it('should be an opossum circuit breaker', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does this matter if everything else works ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not necessarily |
||
expect(breaker).toBeInstanceOf(CircuitBreaker); | ||
}); | ||
|
||
// Tests for circuit breaker functionality can be found in | ||
// __tests__/server/middleware/createRequestHtmlFragment.spec.js | ||
|
||
describe('setEventLoopLagThreshold', () => { | ||
it('should set a default value of 30ms', () => { | ||
setEventLoopLagThreshold(); | ||
expect(getEventLoopLagThreshold()).toBe(30); | ||
}); | ||
|
||
it('should set value to 30ms if input is not a number', () => { | ||
setEventLoopLagThreshold('hello, world'); | ||
expect(getEventLoopLagThreshold()).toBe(30); | ||
}); | ||
|
||
it('should set the given value', () => { | ||
setEventLoopLagThreshold(44); | ||
expect(getEventLoopLagThreshold()).toBe(44); | ||
}); | ||
|
||
it('should handle numbers as strings', () => { | ||
setEventLoopLagThreshold('55'); | ||
expect(getEventLoopLagThreshold()).toBe(55); | ||
}); | ||
}); | ||
}); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ import csp from './csp'; | |
import createFrankLikeFetch from './createFrankLikeFetch'; | ||
|
||
export default { | ||
eventLoopLagThreshold: Infinity, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do these not work with the default ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This prevents the circuit from opening during the integration tests |
||
csp, | ||
corsOrigins: [/\.example.com$/], | ||
configureRequestLog: ({ req, log = {} }) => { | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -20,6 +20,7 @@ import url, { Url } from 'url'; | |||||
import { RouterContext } from '@americanexpress/one-app-router'; | ||||||
import { composeModules } from 'holocron'; | ||||||
import match from '../../universal/utils/matchPromisified'; | ||||||
import breaker from '../utils/circuitBreaker'; | ||||||
|
||||||
import { renderForString, renderForStaticMarkup } from '../utils/reactRendering'; | ||||||
|
||||||
|
@@ -69,11 +70,22 @@ export default function createRequestHtmlFragment({ createRoutes }) { | |||||
}, | ||||||
})); | ||||||
|
||||||
await dispatch(composeModules(routeModules)); | ||||||
|
||||||
const state = store.getState(); | ||||||
const disableScripts = state.getIn(['rendering', 'disableScripts']); | ||||||
const renderPartialOnly = state.getIn(['rendering', 'renderPartialOnly']); | ||||||
|
||||||
if (disableScripts || renderPartialOnly) { | ||||||
await dispatch(composeModules(routeModules)); | ||||||
} else { | ||||||
const fallback = await breaker.fire({ dispatch, modules: routeModules }); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe rename breaker to show its related to getModuleData
Suggested change
|
||||||
|
||||||
if (fallback) { | ||||||
req.appHtml = ''; | ||||||
req.helmetInfo = {}; | ||||||
return next(); | ||||||
} | ||||||
} | ||||||
|
||||||
const renderMethod = (disableScripts || renderPartialOnly) | ||||||
? renderForStaticMarkup : renderForString; | ||||||
|
||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should not really required here, e.g. the breaker should be automatically disabled in tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just needed for the tests that are asserting on the functionality of the breaker