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

Domain Fetcher Circuit Breaker & Circuit Breaker Manager #1756

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
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
65 changes: 65 additions & 0 deletions packages/sui-domain/src/circuitBreaker/CircuitBreaker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const CircuitBreakerStates = {
OPENED: 'OPENED',
CLOSED: 'CLOSED',
HALF: 'HALF'
}

export class CircuitBreaker {
request = null
state = CircuitBreakerStates.CLOSED
failureCount = 0
failureThreshold = 5 // number of failures to determine when to open the circuit
resetAfter = 50000
timeout = 5000 // declare request failure if the function takes more than 5 seconds
Comment on lines +10 to +13
Copy link
Contributor Author

Choose a reason for hiding this comment

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

these default settings can be overridden by passing an options object to the CircuitBreakerManager constructor


constructor({options}) {
this.state = CircuitBreakerStates.CLOSED // allowing requests to go through by default
this.failureCount = 0
// allow request to go through after the circuit has been opened for resetAfter seconds
// open the circuit again if failure is observed, close the circuit otherwise
this.resetAfter = Date.now()
if (options) {
this.failureThreshold = options.failureThreshold
this.timeout = options.timeout
} else {
this.failureThreshold = 5
this.timeout = 5000 // in ms
}
}

async fire(requester, url, options) {
this.request = requester.call(requester, url, options)

if (this.state === CircuitBreakerStates.OPENED) {
if (this.resetAfter <= Date.now()) {
this.state = CircuitBreakerStates.HALF
} else {
throw new Error('Circuit is in open state right now. Please try again later.')
}
}
try {
const response = await this.request
if (response.status === 200) return this.success(response)
return this.failure(response)
} catch (err) {
return this.failure(err.message)
}
}

success(response) {
this.failureCount = 0
if (this.state === CircuitBreakerStates.HALF) {
this.state = CircuitBreakerStates.CLOSED
}
return response
}

failure(response) {
this.failureCount += 1
if (this.state === CircuitBreakerStates.HALF || this.failureCount >= this.failureThreshold) {
this.state = CircuitBreakerStates.OPENED
this.resetAfter = Date.now() + this.timeout
}
return response
}
}
16 changes: 16 additions & 0 deletions packages/sui-domain/src/circuitBreaker/CircuitBreakerManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {CircuitBreaker} from './CircuitBreaker.js'

export default class CircuitBreakerManager {
activeRequests = {}

constructor({options}) {
this._options = options
}

async fire(requester, url, requestOptions) {
const key = `${requestOptions.method}#${url}`
if (!this.activeRequests[key]) this.activeRequests[key] = new CircuitBreaker({options: this._options})

return this.activeRequests[key].fire(requester, url, requestOptions)
}
}
20 changes: 15 additions & 5 deletions packages/sui-domain/src/fetcher/AxiosFetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,24 @@ export default class AxiosFetcher {
this._axios = axios.create(config)
}

get CircuitBreaker() {
return this._config.get('circuitBreaker')
? this._config.get('circuitBreaker')
: {
fire: (requester, url, options, body) => {
requester.call(requester, url, body ?? options, options)
}
}
}

/**
* Get method
* @param {String} url
* @param {Object} options
* @return {Promise<any>}
*/
get(url, options) {
return this._axios.get(url, options)
return this.CircuitBreaker.fire(this._axios.get, url, options)
}

/**
Expand All @@ -30,7 +40,7 @@ export default class AxiosFetcher {
* @return {Promise}
*/
post(url, body, options) {
return this._axios.post(url, body, options)
return this.CircuitBreaker.fire(this._axios.post, url, options, body)
}

/**
Expand All @@ -42,7 +52,7 @@ export default class AxiosFetcher {
* @return {Object}
*/
put(url, body, options) {
return this._axios.put(url, body, options)
return this.CircuitBreaker.fire(this._axios.put, url, options, body)
}

/**
Expand All @@ -54,7 +64,7 @@ export default class AxiosFetcher {
* @return {Object}
*/
patch(url, body, options) {
return this._axios.patch(url, body, options)
return this.CircuitBreaker.fire(this._axios.patch, url, options, body)
}

/**
Expand All @@ -65,6 +75,6 @@ export default class AxiosFetcher {
* @return {Object}
*/
delete(url, options) {
return this._axios.delete(url, options)
return this.CircuitBreaker.fire(this._axios.delete, url, options)
}
}
1 change: 1 addition & 0 deletions packages/sui-domain/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export {default as Entity} from './Entity.js'
export {default as EntryPointFactory} from './EntryPointFactory.js'
export {default as CircuitBreakerManager} from './circuitBreaker/CircuitBreakerManager.js'
export {default as FetcherFactory} from './fetcher/factory.js'
export {default as Mapper} from './Mapper.js'
export {default as Repository} from './Repository.js'
Expand Down
Loading