diff --git a/packages/sui-domain/src/circuitBreaker/CircuitBreaker.js b/packages/sui-domain/src/circuitBreaker/CircuitBreaker.js new file mode 100644 index 000000000..8aed547fd --- /dev/null +++ b/packages/sui-domain/src/circuitBreaker/CircuitBreaker.js @@ -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 + + 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 + } +} diff --git a/packages/sui-domain/src/circuitBreaker/CircuitBreakerManager.js b/packages/sui-domain/src/circuitBreaker/CircuitBreakerManager.js new file mode 100644 index 000000000..8d17782e3 --- /dev/null +++ b/packages/sui-domain/src/circuitBreaker/CircuitBreakerManager.js @@ -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) + } +} diff --git a/packages/sui-domain/src/fetcher/AxiosFetcher.js b/packages/sui-domain/src/fetcher/AxiosFetcher.js index 65631990e..8ead33e7e 100644 --- a/packages/sui-domain/src/fetcher/AxiosFetcher.js +++ b/packages/sui-domain/src/fetcher/AxiosFetcher.js @@ -9,6 +9,17 @@ export default class AxiosFetcher { */ constructor({config}) { this._axios = axios.create(config) + this._config = config + } + + getCircuitBreaker() { + return this._config?.get?.('circuitBreaker') + ? this._config?.get('circuitBreaker') + : { + fire: (requester, verb, url, options, body) => { + return requester[verb].call(this, url, body ?? options, options) + } + } } /** @@ -18,7 +29,7 @@ export default class AxiosFetcher { * @return {Promise} */ get(url, options) { - return this._axios.get(url, options) + return this.getCircuitBreaker().fire(this._axios, 'get', url, options) } /** @@ -30,7 +41,7 @@ export default class AxiosFetcher { * @return {Promise} */ post(url, body, options) { - return this._axios.post(url, body, options) + return this.getCircuitBreaker().fire(this._axios, 'post', url, options, body) } /** @@ -42,7 +53,7 @@ export default class AxiosFetcher { * @return {Object} */ put(url, body, options) { - return this._axios.put(url, body, options) + return this.getCircuitBreaker().fire(this._axios, 'put', url, options, body) } /** @@ -54,7 +65,7 @@ export default class AxiosFetcher { * @return {Object} */ patch(url, body, options) { - return this._axios.patch(url, body, options) + return this.getCircuitBreaker().fire(this._axios, 'patch', url, options, body) } /** @@ -65,6 +76,6 @@ export default class AxiosFetcher { * @return {Object} */ delete(url, options) { - return this._axios.delete(url, options) + return this.getCircuitBreaker().fire(this._axios, 'delete', url, options) } } diff --git a/packages/sui-domain/src/index.js b/packages/sui-domain/src/index.js index 37a6b83c8..38c8ce317 100644 --- a/packages/sui-domain/src/index.js +++ b/packages/sui-domain/src/index.js @@ -1,6 +1,7 @@ export {default as DomainError} from './DomainError.js' 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'