-
Notifications
You must be signed in to change notification settings - Fork 81
feat: Node.js CircuitBreaker with exponential OPEN cooldown — GH#4775 #4793
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
Open
balhar-jakub
wants to merge
17
commits into
v3.x.x
Choose a base branch
from
hermes/gh4775
base: v3.x.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
3d2deef
feat: add CircuitBreaker state machine for EurekaClient retry logic
balhar-jakub 259ad63
feat: integrate CircuitBreaker into EurekaClient heartbeat and regist…
balhar-jakub 18e3eb2
chore: add eslint-disable for no-underscore-dangle in CircuitBreaker.js
balhar-jakub 785aea4
feat: add CircuitBreaker state machine with exponential OPEN cooldown
balhar-jakub b0313e0
fix: EPL-2.0 license only on defaultConfig.js
balhar-jakub 851b389
feat: integrate exponential OPEN cooldown into EurekaClient
balhar-jakub 834ec2d
chore: re-trigger CI
balhar-jakub 4a75243
Merge branch 'v3.x.x' into hermes/gh4775
balhar-jakub 57babc7
Update defaultConfig.js
balhar-jakub 902d59e
Update EurekaClient.js
balhar-jakub 36183bc
Update EurekaClient.test.js
balhar-jakub 61a2c2b
Do not count 404 towards circuit breaker.
balhar-jakub 2c8093f
Update CircuitBreaker.js
balhar-jakub 94ecd86
Merge branch 'v3.x.x' into hermes/gh4775
balhar-jakub aa0921c
fix: address circuit breaker review feedback
balhar-jakub 071811b
fix: apply circuit breaker to initial Eureka registration
balhar-jakub 3afba1c
Merge branch 'v3.x.x' into hermes/gh4775
balhar-jakub File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| /* | ||
| * This program and the accompanying materials are made available under the terms of the | ||
| * Eclipse Public License v2.0 which accompanies this distribution, and is available at | ||
| * https://www.eclipse.org/legal/epl-v20.html | ||
| * | ||
| * SPDX-License-Identifier: EPL-2.0 | ||
| * | ||
| * Copyright Contributors to the Zowe Project. | ||
| */ | ||
|
|
||
| /* eslint-disable no-underscore-dangle */ | ||
| import { EventEmitter } from 'events'; | ||
|
|
||
| const STATES = { | ||
| CLOSED: 'CLOSED', | ||
| OPEN: 'OPEN', | ||
| HALF_OPEN: 'HALF_OPEN', | ||
| }; | ||
|
|
||
| /** | ||
| * Circuit breaker state machine for the Node.js onboarding enabler. | ||
| * | ||
| * Three states: CLOSED (normal), OPEN (failing, no requests), HALF_OPEN (probing). | ||
| * Transitions: | ||
| * CLOSED → OPEN when failureCount >= maxFailures | ||
| * OPEN → HALF_OPEN when cooldown expires (checked via allowRequest()) | ||
| * HALF_OPEN → CLOSED on recordSuccess() | ||
| * HALF_OPEN → OPEN on recordFailure() | ||
| * | ||
| * OPEN cooldown uses exponential backoff: cooldownTime × 2^(openCycleCount-1), | ||
| * capped at backoffMax. CLOSED backoff uses cooldownTime × 2^(failureCount-1). | ||
| * | ||
| * Emits events: 'circuitOpen', 'circuitHalfOpen', 'circuitClose' | ||
| */ | ||
| export default class CircuitBreaker extends EventEmitter { | ||
| /** | ||
| * @param {Object} options | ||
| * @param {number} [options.maxFailures=5] Consecutive failures before circuit opens | ||
| * @param {number} [options.cooldownTime=60000] Base cooldown time in ms (used for | ||
| * both CLOSED exponential backoff and OPEN exponential cooldown base) | ||
| * @param {number} [options.backoffMax=300000] Maximum backoff cap in ms | ||
| */ | ||
| constructor({ | ||
| maxFailures = 5, | ||
| cooldownTime = 60000, | ||
| backoffMax = 300000, | ||
| } = {}) { | ||
| super(); | ||
| this.maxFailures = maxFailures; | ||
| this.cooldownTime = cooldownTime; | ||
| this.backoffMax = backoffMax; | ||
|
|
||
| this._state = STATES.CLOSED; | ||
| this.failureCount = 0; | ||
| this._openedAt = null; | ||
| this._openCycleCount = 0; | ||
| this._halfOpenProbeInFlight = false; | ||
| } | ||
|
|
||
| /** @returns {string} Current state: CLOSED, OPEN, or HALF_OPEN */ | ||
| get state() { | ||
| return this._state; | ||
| } | ||
|
|
||
| /** @returns {boolean} True if circuit is OPEN (not accepting requests) */ | ||
| isOpen() { | ||
| return this._state === STATES.OPEN; | ||
| } | ||
|
|
||
| /** | ||
| * Check whether a request should be allowed. | ||
| * If circuit is OPEN but cooldown has expired, transitions to HALF_OPEN and returns true. | ||
| * | ||
| * @returns {boolean} True if a request may proceed | ||
| */ | ||
| allowRequest() { | ||
| if (this._state === STATES.CLOSED) { | ||
| return true; | ||
| } | ||
| if (this._state === STATES.OPEN && this._cooldownExpired()) { | ||
| this._transitionTo(STATES.HALF_OPEN); | ||
| } | ||
| if (this._state === STATES.HALF_OPEN) { | ||
| if (this._halfOpenProbeInFlight) { | ||
| return false; | ||
| } | ||
| this._halfOpenProbeInFlight = true; | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Record a successful request. Resets failureCount. | ||
| * If transitioning from HALF_OPEN to CLOSED, resets openCycleCount and emits 'circuitClose'. | ||
| * | ||
| * @returns {{ transition: string|null }} Transition if state changed | ||
| */ | ||
| recordSuccess() { | ||
| const prevState = this._state; | ||
| this.failureCount = 0; | ||
| this._halfOpenProbeInFlight = false; | ||
| if (prevState === STATES.HALF_OPEN) { | ||
| this._openCycleCount = 0; | ||
| this._transitionTo(STATES.CLOSED); | ||
| return { transition: STATES.CLOSED }; | ||
| } | ||
| return { transition: null }; | ||
| } | ||
|
|
||
| /** | ||
| * Record a failed request. Increments failureCount. | ||
| * May transition to OPEN if threshold reached. | ||
| * OPEN delay returned is the exponential cooldown . | ||
| * | ||
| * @returns {{ transition: string|null, delay: number }} | ||
| * transition — state change if any; delay — suggested wait before next attempt (ms) | ||
| */ | ||
| recordFailure() { | ||
| this.failureCount += 1; | ||
| const prevState = this._state; | ||
| this._halfOpenProbeInFlight = false; | ||
|
|
||
| // HALF_OPEN probe failure → immediately re-open with exponential delay | ||
| if (prevState === STATES.HALF_OPEN) { | ||
| this._transitionTo(STATES.OPEN); | ||
| return { transition: STATES.OPEN, delay: this._computeOpenCooldown() }; | ||
| } | ||
|
|
||
| // CLOSED + threshold reached → open circuit with exponential delay | ||
| if (prevState === STATES.CLOSED && this.failureCount >= this.maxFailures) { | ||
| this._transitionTo(STATES.OPEN); | ||
| return { transition: STATES.OPEN, delay: this._computeOpenCooldown() }; | ||
| } | ||
|
|
||
| // Still CLOSED, below threshold — return exponential backoff delay | ||
| return { transition: null, delay: this.getNextCooldown() }; | ||
| } | ||
|
|
||
| /** | ||
| * Compute the cooldown/delay for the next scheduling cycle. | ||
| * OPEN state: exponential cooldown based on openCycleCount. | ||
| * CLOSED state: exponential backoff based on failureCount (a single failure uses cooldownTime). | ||
| * Both use cooldownTime as base, capped at backoffMax. | ||
| * | ||
| * @returns {number} Delay in milliseconds | ||
| */ | ||
| getNextCooldown() { | ||
| if (this._state === STATES.OPEN) { | ||
| return this._computeOpenCooldown(); | ||
| } | ||
| if (this.failureCount === 0) { | ||
| return this.cooldownTime; | ||
| } | ||
| const backoff = this.cooldownTime * (2 ** (this.failureCount - 1)); | ||
| return Math.min(backoff, this.backoffMax); | ||
| } | ||
|
balhar-jakub marked this conversation as resolved.
|
||
|
|
||
| /** Reset breaker to CLOSED, zero failures and open cycle count. */ | ||
| reset() { | ||
| this._state = STATES.CLOSED; | ||
| this.failureCount = 0; | ||
| this._openedAt = null; | ||
| this._openCycleCount = 0; | ||
| this._halfOpenProbeInFlight = false; | ||
| } | ||
|
|
||
| // ---- internal ---- | ||
|
|
||
| /** | ||
| * Compute the OPEN cooldown with exponential backoff. | ||
| * cooldownTime × 2^(openCycleCount-1), capped at backoffMax. | ||
| * @returns {number} Cooldown in milliseconds | ||
| */ | ||
| _computeOpenCooldown() { | ||
| if (this._openCycleCount === 0) return this.cooldownTime; | ||
| const backoff = this.cooldownTime * (2 ** (this._openCycleCount - 1)); | ||
| return Math.min(backoff, this.backoffMax); | ||
| } | ||
|
balhar-jakub marked this conversation as resolved.
|
||
|
|
||
| _transitionTo(newState) { | ||
| const oldState = this._state; | ||
| this._state = newState; | ||
|
|
||
| if (newState === STATES.OPEN) { | ||
| this._openedAt = performance.now(); | ||
| this._openCycleCount += 1; | ||
| this.emit('circuitOpen', { from: oldState, to: newState }); | ||
| } else if (newState === STATES.HALF_OPEN) { | ||
| this.emit('circuitHalfOpen', { from: oldState, to: newState }); | ||
| } else if (newState === STATES.CLOSED) { | ||
| this._openedAt = null; | ||
| this.emit('circuitClose', { from: oldState, to: newState }); | ||
| } | ||
| } | ||
|
|
||
| _cooldownExpired() { | ||
| if (this._openedAt === null) { | ||
| return false; | ||
| } | ||
| return (performance.now() - this._openedAt) >= this._computeOpenCooldown(); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.