From 043a32c1b3a34cd86cc52d9f8660995c662ba470 Mon Sep 17 00:00:00 2001 From: "jordi.munoz@adevinta.com" Date: Fri, 3 May 2024 12:51:02 +0200 Subject: [PATCH 1/9] feat(packages/sui-domain): wrap axios http verbs calls with Circuit Breaker --- .../sui-domain/src/fetcher/AxiosFetcher.js | 12 ++-- .../sui-domain/src/fetcher/CircuitBreaker.js | 64 +++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 packages/sui-domain/src/fetcher/CircuitBreaker.js diff --git a/packages/sui-domain/src/fetcher/AxiosFetcher.js b/packages/sui-domain/src/fetcher/AxiosFetcher.js index 65631990e..f877e2793 100644 --- a/packages/sui-domain/src/fetcher/AxiosFetcher.js +++ b/packages/sui-domain/src/fetcher/AxiosFetcher.js @@ -1,5 +1,7 @@ import axios from 'axios' +import {CircuitBreaker} from './CircuitBreaker.js' + /** @typedef {import('./FetcherInterface').default} FetcherInterface */ /** @implements {FetcherInterface} */ export default class AxiosFetcher { @@ -18,7 +20,7 @@ export default class AxiosFetcher { * @return {Promise} */ get(url, options) { - return this._axios.get(url, options) + return new CircuitBreaker(this._axios.get(url, options)).fire() } /** @@ -30,7 +32,7 @@ export default class AxiosFetcher { * @return {Promise} */ post(url, body, options) { - return this._axios.post(url, body, options) + return new CircuitBreaker(this._axios.post(url, body, options)).fire() } /** @@ -42,7 +44,7 @@ export default class AxiosFetcher { * @return {Object} */ put(url, body, options) { - return this._axios.put(url, body, options) + return new CircuitBreaker(this._axios.put(url, body, options)).fire() } /** @@ -54,7 +56,7 @@ export default class AxiosFetcher { * @return {Object} */ patch(url, body, options) { - return this._axios.patch(url, body, options) + return new CircuitBreaker(this._axios.patch(url, body, options)).fire() } /** @@ -65,6 +67,6 @@ export default class AxiosFetcher { * @return {Object} */ delete(url, options) { - return this._axios.delete(url, options) + return new CircuitBreaker(this._axios.delete(url, options)).fire() } } diff --git a/packages/sui-domain/src/fetcher/CircuitBreaker.js b/packages/sui-domain/src/fetcher/CircuitBreaker.js new file mode 100644 index 000000000..703baacc0 --- /dev/null +++ b/packages/sui-domain/src/fetcher/CircuitBreaker.js @@ -0,0 +1,64 @@ +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(request, options) { + this.request = request + 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() { + 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.data) + return this.failure(response.data) + } catch (err) { + return this.failure(err.message) + } + } + + success(data) { + this.failureCount = 0 + if (this.state === CircuitBreakerStates.HALF) { + this.state = CircuitBreakerStates.CLOSED + } + return data + } + + failure(data) { + this.failureCount += 1 + if (this.state === CircuitBreakerStates.HALF || this.failureCount >= this.failureThreshold) { + this.state = CircuitBreakerStates.OPENED + this.resetAfter = Date.now() + this.timeout + } + return data + } +} From b64a94bd6813aba2e6d638573062abf69fa8c00a Mon Sep 17 00:00:00 2001 From: "jordi.munoz@adevinta.com" Date: Fri, 3 May 2024 13:12:16 +0200 Subject: [PATCH 2/9] feat(packages/sui-domain): adjust circuit breaker code --- packages/sui-domain/src/fetcher/CircuitBreaker.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/sui-domain/src/fetcher/CircuitBreaker.js b/packages/sui-domain/src/fetcher/CircuitBreaker.js index 703baacc0..eff873150 100644 --- a/packages/sui-domain/src/fetcher/CircuitBreaker.js +++ b/packages/sui-domain/src/fetcher/CircuitBreaker.js @@ -38,27 +38,27 @@ export class CircuitBreaker { } try { const response = await this.request - if (response.status === 200) return this.success(response.data) - return this.failure(response.data) + if (response.status === 200) return this.success(response) + return this.failure(response) } catch (err) { return this.failure(err.message) } } - success(data) { + success(response) { this.failureCount = 0 if (this.state === CircuitBreakerStates.HALF) { this.state = CircuitBreakerStates.CLOSED } - return data + return response } - failure(data) { + 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 data + return response } } From 69f8599c0db7223703f40fef65c3c7b4854c76f5 Mon Sep 17 00:00:00 2001 From: "jordi.munoz@adevinta.com" Date: Thu, 16 May 2024 15:41:40 +0200 Subject: [PATCH 3/9] feat(packages/sui-domain): changes on fetcher --- .../sui-domain/src/fetcher/AxiosFetcher.js | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/sui-domain/src/fetcher/AxiosFetcher.js b/packages/sui-domain/src/fetcher/AxiosFetcher.js index f877e2793..d4969c4f9 100644 --- a/packages/sui-domain/src/fetcher/AxiosFetcher.js +++ b/packages/sui-domain/src/fetcher/AxiosFetcher.js @@ -1,7 +1,5 @@ import axios from 'axios' -import {CircuitBreaker} from './CircuitBreaker.js' - /** @typedef {import('./FetcherInterface').default} FetcherInterface */ /** @implements {FetcherInterface} */ export default class AxiosFetcher { @@ -13,6 +11,16 @@ 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 @@ -20,7 +28,7 @@ export default class AxiosFetcher { * @return {Promise} */ get(url, options) { - return new CircuitBreaker(this._axios.get(url, options)).fire() + return this.CircuitBreaker.fire(this._axios.get, url, options) } /** @@ -32,7 +40,7 @@ export default class AxiosFetcher { * @return {Promise} */ post(url, body, options) { - return new CircuitBreaker(this._axios.post(url, body, options)).fire() + return this.CircuitBreaker.fire(this._axios.post, url, options, body) } /** @@ -44,7 +52,7 @@ export default class AxiosFetcher { * @return {Object} */ put(url, body, options) { - return new CircuitBreaker(this._axios.put(url, body, options)).fire() + return this.CircuitBreaker.fire(this._axios.put, url, options, body) } /** @@ -56,7 +64,7 @@ export default class AxiosFetcher { * @return {Object} */ patch(url, body, options) { - return new CircuitBreaker(this._axios.patch(url, body, options)).fire() + return this.CircuitBreaker.fire(this._axios.patch, url, options, body) } /** @@ -67,6 +75,6 @@ export default class AxiosFetcher { * @return {Object} */ delete(url, options) { - return new CircuitBreaker(this._axios.delete(url, options)).fire() + return this.CircuitBreaker.fire(this._axios.delete, url, options) } } From 9bc7b1fc092a2a797bffb8b3fbbd315e0cc8ff16 Mon Sep 17 00:00:00 2001 From: "jordi.munoz@adevinta.com" Date: Thu, 16 May 2024 15:42:27 +0200 Subject: [PATCH 4/9] feat(packages/sui-domain): implement circuit breaker and circuit breaker manager --- .../CircuitBreaker.js | 7 ++++--- .../src/circuitBreaker/CircuitBreakerManager.js | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) rename packages/sui-domain/src/{fetcher => circuitBreaker}/CircuitBreaker.js (93%) create mode 100644 packages/sui-domain/src/circuitBreaker/CircuitBreakerManager.js diff --git a/packages/sui-domain/src/fetcher/CircuitBreaker.js b/packages/sui-domain/src/circuitBreaker/CircuitBreaker.js similarity index 93% rename from packages/sui-domain/src/fetcher/CircuitBreaker.js rename to packages/sui-domain/src/circuitBreaker/CircuitBreaker.js index eff873150..8aed547fd 100644 --- a/packages/sui-domain/src/fetcher/CircuitBreaker.js +++ b/packages/sui-domain/src/circuitBreaker/CircuitBreaker.js @@ -12,8 +12,7 @@ export class CircuitBreaker { resetAfter = 50000 timeout = 5000 // declare request failure if the function takes more than 5 seconds - constructor(request, options) { - this.request = request + 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 @@ -28,7 +27,9 @@ export class CircuitBreaker { } } - async fire() { + 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 diff --git a/packages/sui-domain/src/circuitBreaker/CircuitBreakerManager.js b/packages/sui-domain/src/circuitBreaker/CircuitBreakerManager.js new file mode 100644 index 000000000..cc44acc54 --- /dev/null +++ b/packages/sui-domain/src/circuitBreaker/CircuitBreakerManager.js @@ -0,0 +1,16 @@ +import {CircuitBreaker} from './CircuitBreaker.js' + +export 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) + } +} From 09b2ce729ecf86e69aa2efed707aff3ac2204019 Mon Sep 17 00:00:00 2001 From: "jordi.munoz@adevinta.com" Date: Thu, 16 May 2024 15:43:58 +0200 Subject: [PATCH 5/9] feat(packages/sui-domain): export default circuit breaker manager --- packages/sui-domain/src/circuitBreaker/CircuitBreakerManager.js | 2 +- packages/sui-domain/src/index.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sui-domain/src/circuitBreaker/CircuitBreakerManager.js b/packages/sui-domain/src/circuitBreaker/CircuitBreakerManager.js index cc44acc54..8d17782e3 100644 --- a/packages/sui-domain/src/circuitBreaker/CircuitBreakerManager.js +++ b/packages/sui-domain/src/circuitBreaker/CircuitBreakerManager.js @@ -1,6 +1,6 @@ import {CircuitBreaker} from './CircuitBreaker.js' -export class CircuitBreakerManager { +export default class CircuitBreakerManager { activeRequests = {} constructor({options}) { diff --git a/packages/sui-domain/src/index.js b/packages/sui-domain/src/index.js index ec5c03a3b..94bf80d2c 100644 --- a/packages/sui-domain/src/index.js +++ b/packages/sui-domain/src/index.js @@ -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' From 51e7bbc774fd8c3068875e30a61c0365b63212c5 Mon Sep 17 00:00:00 2001 From: "jordi.munoz@adevinta.com" Date: Fri, 17 May 2024 12:25:30 +0200 Subject: [PATCH 6/9] feat(packages/sui-domain): return requester.call call --- packages/sui-domain/src/fetcher/AxiosFetcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sui-domain/src/fetcher/AxiosFetcher.js b/packages/sui-domain/src/fetcher/AxiosFetcher.js index d4969c4f9..30d68c55a 100644 --- a/packages/sui-domain/src/fetcher/AxiosFetcher.js +++ b/packages/sui-domain/src/fetcher/AxiosFetcher.js @@ -16,7 +16,7 @@ export default class AxiosFetcher { ? this._config.get('circuitBreaker') : { fire: (requester, url, options, body) => { - requester.call(requester, url, body ?? options, options) + return requester.call(requester, url, body ?? options, options) } } } From 05e55e15e2bfafcaae81b90b268c85d48cb8b3be Mon Sep 17 00:00:00 2001 From: "jordi.munoz@adevinta.com" Date: Wed, 12 Jun 2024 14:04:47 +0200 Subject: [PATCH 7/9] refactor(packages/sui-domain): use right getter --- packages/sui-domain/src/fetcher/AxiosFetcher.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/sui-domain/src/fetcher/AxiosFetcher.js b/packages/sui-domain/src/fetcher/AxiosFetcher.js index 30d68c55a..bf12bb0d4 100644 --- a/packages/sui-domain/src/fetcher/AxiosFetcher.js +++ b/packages/sui-domain/src/fetcher/AxiosFetcher.js @@ -11,7 +11,7 @@ export default class AxiosFetcher { this._axios = axios.create(config) } - get CircuitBreaker() { + getCircuitBreaker() { return this._config.get('circuitBreaker') ? this._config.get('circuitBreaker') : { @@ -28,7 +28,7 @@ export default class AxiosFetcher { * @return {Promise} */ get(url, options) { - return this.CircuitBreaker.fire(this._axios.get, url, options) + return this.getCircuitBreaker().fire(this._axios.get, url, options) } /** @@ -40,7 +40,7 @@ export default class AxiosFetcher { * @return {Promise} */ post(url, body, options) { - return this.CircuitBreaker.fire(this._axios.post, url, options, body) + return this.getCircuitBreaker().fire(this._axios.post, url, options, body) } /** @@ -52,7 +52,7 @@ export default class AxiosFetcher { * @return {Object} */ put(url, body, options) { - return this.CircuitBreaker.fire(this._axios.put, url, options, body) + return this.getCircuitBreaker().fire(this._axios.put, url, options, body) } /** @@ -64,7 +64,7 @@ export default class AxiosFetcher { * @return {Object} */ patch(url, body, options) { - return this.CircuitBreaker.fire(this._axios.patch, url, options, body) + return this.getCircuitBreaker().fire(this._axios.patch, url, options, body) } /** @@ -75,6 +75,6 @@ export default class AxiosFetcher { * @return {Object} */ delete(url, options) { - return this.CircuitBreaker.fire(this._axios.delete, url, options) + return this.getCircuitBreaker().fire(this._axios.delete, url, options) } } From 416357e6b1e02c65fe11c136727806bb601090eb Mon Sep 17 00:00:00 2001 From: "jordi.munoz@adevinta.com" Date: Fri, 14 Jun 2024 10:51:20 +0200 Subject: [PATCH 8/9] refactor(Root): refactor axios fetcher to make it work with circuit breaker --- .../sui-domain/src/fetcher/AxiosFetcher.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/sui-domain/src/fetcher/AxiosFetcher.js b/packages/sui-domain/src/fetcher/AxiosFetcher.js index bf12bb0d4..d2bcb4f8c 100644 --- a/packages/sui-domain/src/fetcher/AxiosFetcher.js +++ b/packages/sui-domain/src/fetcher/AxiosFetcher.js @@ -9,14 +9,15 @@ export default class AxiosFetcher { */ constructor({config}) { this._axios = axios.create(config) + this._config = config } getCircuitBreaker() { - return this._config.get('circuitBreaker') - ? this._config.get('circuitBreaker') + return this._config?.get && this._config?.get('circuitBreaker') + ? this._config?.get('circuitBreaker') : { - fire: (requester, url, options, body) => { - return requester.call(requester, url, body ?? options, options) + fire: (requester, verb, url, options, body) => { + return requester[verb].call(this, url, body ?? options, options) } } } @@ -28,7 +29,7 @@ export default class AxiosFetcher { * @return {Promise} */ get(url, options) { - return this.getCircuitBreaker().fire(this._axios.get, url, options) + return this.getCircuitBreaker().fire(this._axios, 'get', url, options) } /** @@ -40,7 +41,7 @@ export default class AxiosFetcher { * @return {Promise} */ post(url, body, options) { - return this.getCircuitBreaker().fire(this._axios.post, url, options, body) + return this.getCircuitBreaker().fire(this._axios, 'post', url, options, body) } /** @@ -52,7 +53,7 @@ export default class AxiosFetcher { * @return {Object} */ put(url, body, options) { - return this.getCircuitBreaker().fire(this._axios.put, url, options, body) + return this.getCircuitBreaker().fire(this._axios, 'put', url, options, body) } /** @@ -64,7 +65,7 @@ export default class AxiosFetcher { * @return {Object} */ patch(url, body, options) { - return this.getCircuitBreaker().fire(this._axios.patch, url, options, body) + return this.getCircuitBreaker().fire(this._axios, 'patch', url, options, body) } /** @@ -75,6 +76,6 @@ export default class AxiosFetcher { * @return {Object} */ delete(url, options) { - return this.getCircuitBreaker().fire(this._axios.delete, url, options) + return this.getCircuitBreaker().fire(this._axios, 'delete', url, options) } } From 89e71986c3197f54b24ac85b30439beed8232098 Mon Sep 17 00:00:00 2001 From: "jordi.munoz@adevinta.com" Date: Mon, 17 Jun 2024 12:38:18 +0200 Subject: [PATCH 9/9] refactor(packages/sui-domain): simplify syntax --- packages/sui-domain/src/fetcher/AxiosFetcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sui-domain/src/fetcher/AxiosFetcher.js b/packages/sui-domain/src/fetcher/AxiosFetcher.js index d2bcb4f8c..8ead33e7e 100644 --- a/packages/sui-domain/src/fetcher/AxiosFetcher.js +++ b/packages/sui-domain/src/fetcher/AxiosFetcher.js @@ -13,7 +13,7 @@ export default class AxiosFetcher { } getCircuitBreaker() { - return this._config?.get && this._config?.get('circuitBreaker') + return this._config?.get?.('circuitBreaker') ? this._config?.get('circuitBreaker') : { fire: (requester, verb, url, options, body) => {