-
-
Notifications
You must be signed in to change notification settings - Fork 330
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
Preventing MaxListenersExceededWarning #4421
Conversation
Performance Report✔️ no performance regression detected Full benchmark results
|
// if any before setting a new one. | ||
if (this.emitter.listenerCount(ValidatorEvent.chainHead) > 0) { | ||
this.emitter.off(ValidatorEvent.chainHead, headListener); | ||
} | ||
this.emitter.on(ValidatorEvent.chainHead, headListener); |
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 solution looks very strange, I don't follow the argument. The handler headListener
is created in this scope so it can be proven that headListener
has never been subscribed to this event, it's impossible. So the line this.emitter.off(ValidatorEvent.chainHead, headListener)
is useless.
I also want see first a hard proof that the MaxListenersExceededWarning comes actually from this specific listener, and not from sleep()
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.
The handler headListener is created in this scope so it can be proven that headListener has never been subscribed to this event, it's impossible
In the case where waitForBlockSlot
is called multiple times (before the unsubscribe), it is possible to have headListener
subscribed multiple times. The intention of the check is to prevent this. But I will look into this again to ascertain your concern.
I also want see first a hard proof that the MaxListenersExceededWarning comes actually from this specific listener, and not from sleep()
I am almost certain it does not come from the sleep
as no where in the sleep
code path is EventEmitter
used. The sleep
code path makes use of AbortSignal
and from what I see, that uses a different API. But will try and reproduce and confirm this.
// We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10 | ||
// Since it is perfectly fine to have listeners > 10 | ||
emitter.setMaxListeners(Infinity); | ||
setMaxListeners(Infinity, signal); |
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.
Can you commit as comment the stack trace proving that this very specific event emitter gets more that 10 listeners?
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 could not create a situation where the warning is triggered locally. So I have removed the changes.
@@ -118,6 +119,9 @@ export class BeaconNode { | |||
metricsRegistries = [], | |||
}: IBeaconNodeInitModules): Promise<T> { | |||
const controller = new AbortController(); | |||
// We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10 | |||
// Since it is perfectly fine to have listeners > 10 | |||
setMaxListeners(Infinity, controller.signal); |
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.
Can you commit as comment the stack trace proving that this very specific event emitter gets more that 10 listeners?
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.
The signal here was what was passed down and triggered this:
(node:56301) MaxListenersExceededWarning: Possible EventTarget memory leak detected. 11 abort listeners added to [AbortSignal]. Use events.setMaxListeners() to increase limit
at AbortSignal.[kNewListener] (node:internal/event_target:426:17)
at AbortSignal.[kNewListener] (node:internal/abort_controller:173:24)
at AbortSignal.addEventListener (node:internal/event_target:535:23)
at JsonRpcHttpClient.fetchJsonOneUrl (file:///Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/packages/beacon-node/src/eth1/provider/jsonRpcHttpClient.ts:194:24)
at JsonRpcHttpClient.fetchJson (file:///Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/packages/beacon-node/src/eth1/provider/jsonRpcHttpClient.ts:160:27)
at retry.retries._b (file:///Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/packages/beacon-node/src/eth1/provider/jsonRpcHttpClient.ts:125:27)
at retry (file:///Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/packages/utils/src/retry.ts:33:20)
at JsonRpcHttpClient.fetchWithRetries (file:///Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/packages/beacon-node/src/eth1/provider/jsonRpcHttpClient.ts:119:23)
at ExecutionEngineHttp.exchangeTransitionConfigurationV1 (file:///Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/packages/beacon-node/src/execution/engine/http.ts:298:27)
at BeaconChain.exchangeTransitionConfiguration (file:///Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/packages/beacon-node/src/chain/chain.ts:573:62)
Aug-17 21:32:00.064[CHAI
@@ -52,7 +52,7 @@ export class AttestationService { | |||
// A validator should create and broadcast the attestation to the associated attestation subnet when either | |||
// (a) the validator has received a valid block from the expected block proposer for the assigned slot or | |||
// (b) one-third of the slot has transpired (SECONDS_PER_SLOT / 3 seconds after the start of slot) -- whichever comes first. | |||
await Promise.race([sleep(this.clock.msToSlot(slot + 1 / 3), signal), this.waitForBlockSlot(slot)]); | |||
await Promise.race([this.waitForBlockSlot(slot), sleep(this.clock.msToSlot(slot + 1 / 3), signal)]); |
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.
Why is it necessary to change the order 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.
It was not a necessity. It was changed to align with the text of the spec, which was placed as a comment
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.
What do you mean the spec? Where does the spec specify the order of promises in javascript?
@@ -41,6 +42,10 @@ export async function beaconHandler(args: IBeaconArgs & IGlobalArgs): Promise<vo | |||
const options = beaconNodeOptions.getWithDefaults(); | |||
|
|||
const abortController = new AbortController(); | |||
// We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10 | |||
// Since it is perfectly fine to have listeners > 10 | |||
setMaxListeners(Infinity, abortController.signal); |
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.
Can you commit as comment the stack trace proving that this very specific event emitter gets more that 10 listeners?
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 could not create a situation where the warning is triggered locally. So I have removed the changes
packages/validator/src/validator.ts
Outdated
@@ -85,6 +86,10 @@ export class Validator { | |||
} = opts; | |||
const config = createIBeaconConfig(dbOps.config, genesis.genesisValidatorsRoot); | |||
this.controller = new AbortController(); | |||
// We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10 | |||
// Since it is perfectly fine to have listeners > 10 | |||
setMaxListeners(Infinity, this.controller.signal); |
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.
Can you commit as comment the stack trace proving that this very specific event emitter gets more that 10 listeners? Same for below
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 removed the changes for the abort controller, as I could find a situation where it triggered the warning. I left the one for ValidatorEventEmitter, even though I could not recreate the situation that triggered it, I did observe by chance back in January. See #3569. The log reproduced here
(node:15159) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 chainHead listeners added to [ValidatorEventEmitter]. Use emitter.setMaxListeners() to increase limit
@@ -71,6 +72,9 @@ export class ReqResp implements IReqResp { | |||
|
|||
async start(): Promise<void> { | |||
this.controller = new AbortController(); | |||
// We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10 | |||
// Since it is perfectly fine to have listeners > 10 | |||
setMaxListeners(Infinity, this.controller.signal); |
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.
Responsible for the warning observed in the trace:
(node:56301) MaxListenersExceededWarning: Possible EventTarget memory leak detected. 11 abort listeners added to [AbortSignal]. Use events.setMaxListeners() to increase limit
at AbortSignal.[kNewListener] (node:internal/event_target:426:17)
at AbortSignal.[kNewListener] (node:internal/abort_controller:173:24)
at AbortSignal.addEventListener (node:internal/event_target:535:23)
at anySignal (/Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/node_modules/any-signal/index.js:27:12)
at withTimeout (file:///Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/packages/utils/src/timeout.ts:11:34)
at sendRequest (file:///Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/packages/beacon-node/src/network/reqresp/request/index.ts:87:50)
at ReqResp.sendRequest (file:///Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/packages/beacon-node/src/network/reqresp/reqResp.ts:154:28)
at ReqResp.status (file:///Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/packages/beacon-node/src/network/reqresp/reqResp.ts:92:23)
at PeerManager.requestStatus (file:///Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/packages/beacon-node/src/network/peers/peerManager.ts:359:46)
at file:///Users/dadepoaderemi/Documents/work/chainsafe/official/lodestar/packages/beacon-node/src/network/peers/peerManager.ts:368:56
private reqRespHandlers: ReqRespHandlers; | ||
private metadataController: MetadataController; | ||
private peerRpcScores: IPeerRpcScoreStore; | ||
private inboundRateLimiter: IRateLimiter; | ||
private networkEventBus: INetworkEventBus; | ||
private controller = new AbortController(); |
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.
Why do you need to initialize the AbortController on constructor?
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 sure why this is showing as a diff here, but this is not newly introduced. See it on unstable
branch. See here
@@ -121,6 +121,10 @@ export class Validator { | |||
} | |||
|
|||
const emitter = new ValidatorEventEmitter(); | |||
// Validator event emitter can have more than 10 listeners in a normal course of operation | |||
// We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10 | |||
emitter.setMaxListeners(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.
Can you add proof with stack trace that this event listener triggers warnings?
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.
Already responded to the diff in the comment 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.
LGTM
@@ -1,3 +1,4 @@ | |||
import {setMaxListeners} from "node:events"; |
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.
Shame we have to introduce a nodejs specific api here.
Later we can pass down a signal from the top-level beacon node and avoid creating a new signal here.
Motivation
See #3569 and # #4394 for bug report
Description
The waitForBlockSlot should execute once per slot and per slot there should be a ValidatorEvent.chainHead event emitted In the unexpected situation where ValidatorEvent.chainHead is never emitted or is emitted but takes longer to propagate to the validator, to prevent accumulating listeners for ValidatorEvent.chainHead and triggering MaxListenersExceededWarning.
This PR clears listener for ValidatorEvent.chainHead if any before setting a new one.
Closes #3569
Beacon Node
Validator Client
Spent some time trying to reproducing the warning for the validator client but could not. Even though back in January I observed the warning emanating from
ValidatorEventEmitter
see #3569