-
-
Notifications
You must be signed in to change notification settings - Fork 81
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
fix: internal listeners infinite retry loop #284
base: main
Are you sure you want to change the base?
Conversation
It would be good to cover this with tests |
src/PollingBlockTracker.ts
Outdated
@@ -54,6 +52,10 @@ export class PollingBlockTracker | |||
|
|||
private readonly _setSkipCacheFlag: boolean; | |||
|
|||
readonly #onLatestBlockInternalListeners: (( |
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.
Other class variables use private readonly _
, but I suppose we'll want to change them sometime in the future.
I'm ok with aligning this with others for now if that looks better
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
This reverts commit eb5514d.
src/PollingBlockTracker.ts
Outdated
const latestBlock: string = await new Promise((resolve) => | ||
this.once('latest', resolve), | ||
); | ||
const latestBlock: string = await new Promise((resolve) => { |
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.
To my understanding this change allows the PollingBlockTracker
instance to be shut down (i.e. network calls will stop).
Though this promise here will never be rejected, meaning that in the case of:
PollingBlockTracker
being stopped, and- Desired network not available
The promise will be hanging forever.
I believe the fix should also reject this promise in the above scenario, resuming the caller execution
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.
Done in 3f39061
this.#addInternalListener(onLatestBlockAvailable); | ||
this.#addInternalListener(onLatestBlockUnavailable); | ||
this.once('latest', onLatestBlockAvailable); | ||
this.on('error', onLatestBlockUnavailable); |
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 listener is attached using .on
instead of .once
because the block tracker could be stopped after several errors, and we need to catch all of them
); | ||
const latestBlock: string = await new Promise((resolve, reject) => { | ||
// eslint-disable-next-line prefer-const | ||
let onLatestBlockUnavailable: InternalListener; |
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 is first declared as uninitialized so that we can pass its reference to onLatestBlockAvailable
. Unfortunately, it looks like eslint is unable to capture that
if ( | ||
this.listeners('error').filter((listener) => | ||
this.#internalEventListeners.every( | ||
(internalListener) => !Object.is(listener, internalListener), | ||
), | ||
).length === 0 | ||
) { | ||
console.error(newErr); | ||
} |
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 behavior of this try / catch block is unclear to me. It looks like we want to log the error when this.emit('error', newErr);
throws, but looking at unit tests what we want is log the error when there is no listener attached to the error
event.
Since we now have an internal listener on error
as well, we need to exclude that
this.#addInternalListener(onLatestBlockAvailable); | ||
this.#addInternalListener(onLatestBlockUnavailable); | ||
this.once('latest', onLatestBlockAvailable); | ||
this.on('error', onLatestBlockUnavailable); |
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.
Hmm, I'm thinking that this would not cover the scenario where the block tracker is destroyed before fetching the first block and without throwing errors.
We can probably listen to _ended
instead
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 looks like _ended
could present other issues, because of the weird order of events received:
_start
is emitted- Latest block is fetched from the provider
_ended
event listener is executedlatest
event listener is executed
Our block tracker implementation relies on the presence of listeners to establish whether the polling should be continued or stopped.
However, an internal listener is being added in the
getLatestBlock
method, which will count as the other external listeners and will prevent the instance from stopping fetching new blocks. This creates an infinite loop in case the network is unreachable, because of the retry mechanism.This PR aims to fix this behavior by keeping track of internal listeners' references in order to exclude them from the listener's count
Fixes #163