-
Notifications
You must be signed in to change notification settings - Fork 86
feat(createRequestHtmlFragment): implemented circuit breaker #111
Conversation
@@ -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 comment
The 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
checked locally, expect.assertions(5);
can be removed.
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.
expect.assertions ensures we get passed the promise to all the assertions
} 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
not necessarily
@@ -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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
This prevents the circuit from opening during the integration tests
e88695d
to
31da485
Compare
📊 Bundle Size Report
|
const realHrtime = process.hrtime; | ||
const mockHrtime = (...args) => realHrtime(...args); | ||
mockHrtime.bigint = jest.fn(); | ||
process.hrtime = mockHrtime; |
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
src/server/utils/circuitBreaker.js
Outdated
const breaker = new CircuitBreaker(getModuleData, options); | ||
// Just need to connect opossum to prometheus | ||
// eslint-disable-next-line no-unused-vars | ||
const metrics = new PrometheusMetrics(breaker, register); |
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 would probably not expose this at this level. Why was it added here?
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.
Just keeping it close to the code, but yes it makes more since to go in src/server/metrics
src/server/utils/circuitBreaker.js
Outdated
} | ||
}, 100); | ||
|
||
export default breaker; |
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 a more reusable and testable implementation would be to make this a factory, so we can avoid pulling in holocron in here, but rather have the getModuleData
passed in to the factory instead.
src/server/utils/circuitBreaker.js
Outdated
let eventLoopLagThreshold = 30; | ||
|
||
export const setEventLoopLagThreshold = (n) => { | ||
eventLoopLagThreshold = Number(n) || 30; |
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.
@mcollina what are your thoughts on this default value?
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.
30ms is very aggressive. I would increase quite a bit. The 100-500ms range seems more ok for an SSR app (most React rendering go up to 80-100ms).
src/server/utils/circuitBreaker.js
Outdated
breaker.healthCheck(async () => { | ||
if (!getModule(rootModuleName)) return; | ||
const start = process.hrtime.bigint(); | ||
await immediate(); |
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.
You might want to use https://nodejs.org/dist/latest-v12.x/docs/api/perf_hooks.html#perf_hooks_perf_hooks_monitoreventloopdelay_options to measure event loop latency. It adds less overhead.
…express/one-app into feature/circuit-breaker
@mcollina I did some refactoring here, think you could review again? |
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.
lgtm
@@ -18,6 +18,7 @@ import csp from './csp'; | |||
import createFrankLikeFetch from './createFrankLikeFetch'; | |||
|
|||
export default { | |||
eventLoopDelayThreshold: Infinity, |
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.
Should this be set to a smaller amount?
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 to prevent the circuit from opening during the integration tests
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 comment
The reason will be displayed to describe this comment to others. Learn more.
maybe rename breaker to show its related to getModuleData
const fallback = await breaker.fire({ dispatch, modules: routeModules }); | |
const fallback = await getModuleDataBreaker.fire({ dispatch, modules: routeModules }); |
Description
This implements a circuit breaker using opossum. The circuit opens when event loop delay is > 250ms (by default- is configurable). When the circuit is open there is no SSR. The circuit will go into a half open state after 10s. At that point it will attempt the full request again, the circuit will close.
Motivation and Context
This provides significant performance improvements.
How Has This Been Tested?
In addition to the unit tests, I did some load testing locally. I ran a json server using
json-server http://jsonplaceholder.typicode.com/db -d 800 -p 1337
, then changed SSR Frank to make its requests tohttp://localhost:1337/todos
. Once that was set up I called autocannon withautocannon localhost:3000/healthy-frank/ssr-frank -c 200 -d 30 --headers "Accept-Language= en-USen;q=0.5" --headers "User-Agent= curl/7.5.0"
. Below are the results for master and for this branch.(Note the circuit breaker is disabled for perf tests by setting
eventLoopDelayThreshold
toInfinity
. To validate locally, that line should be removed.master
This branch
Types of Changes
Checklist:
What is the Impact to Developers Using One App?
No impact to developers other than SSR turning off in the cases that the circuit opens