Skip to content
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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

mikesposito
Copy link
Member

@mikesposito mikesposito commented Nov 13, 2024

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

@mikesposito mikesposito requested a review from a team as a code owner November 13, 2024 13:15
@mikesposito
Copy link
Member Author

It would be good to cover this with tests

@@ -54,6 +52,10 @@ export class PollingBlockTracker

private readonly _setSkipCacheFlag: boolean;

readonly #onLatestBlockInternalListeners: ((
Copy link
Member Author

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

const latestBlock: string = await new Promise((resolve) =>
this.once('latest', resolve),
);
const latestBlock: string = await new Promise((resolve) => {
Copy link
Member Author

@mikesposito mikesposito Nov 20, 2024

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

Copy link
Member Author

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);
Copy link
Member Author

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;
Copy link
Member Author

@mikesposito mikesposito Nov 20, 2024

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

Comment on lines +348 to +356
if (
this.listeners('error').filter((listener) =>
this.#internalEventListeners.every(
(internalListener) => !Object.is(listener, internalListener),
),
).length === 0
) {
console.error(newErr);
}
Copy link
Member Author

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);
Copy link
Member Author

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

Copy link
Member Author

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:

  1. _start is emitted
  2. Latest block is fetched from the provider
  3. _ended event listener is executed
  4. latest event listener is executed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Endless failed request polling loop
2 participants