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

Provider related improvements #3190

Merged
merged 184 commits into from
Apr 6, 2020
Merged

Provider related improvements #3190

merged 184 commits into from
Apr 6, 2020

Conversation

nivida
Copy link
Contributor

@nivida nivida commented Nov 8, 2019

Description

Fixes #1648, #3042, #3092, #1085, #1391, #1558, #1852, #1646, #1510 and #1933

Because PR #3187, #3156, #3171, and #3167 do all depend strongly on each other and it's not possible to finish them separately. Did we move all of those PRs together and do improve and extend the provider handling of web3.js here in one bigger PR.

RequestManager

The provider will now only get resolved once and the data listener is no longer added on each initiation of a Module, Contract, or on executing of the setProvider method on each package.

Old Behaviour

  1. call setProvider.
  2. create RequestManager and register data listener.
  3. call setProvider on all children's.
  4. create RequestManager and register data listener for each child package.

New Behaviour

  1. call setProvider.
  2. create RequestManager and register data listener.
  3. call setRequestManager on all children's.

By side of improving the inheritance logic of the RequestManager did we add the error and connect event listener to pass errors to subscriptions and to call resubscribe on each subscription if the currently used provider has re-established his connection.

WebsocketProvider

connection not open error:

Instead of just waiting 10ms with a setTimeout and to try it again does it now add the item to the requestQueue and will get executed as soon as the connect event does get emitted. If the connection does emit an error or close event does it terminate the request correctly as well.

Reconnecting:

Options:

{
  reconnect: {
    auto: false,
    delay: 5000,
    maxAttempts: false,
    onTimeout: false
  }
}

Websocket reconnecting is a long-standing issue in the 1.x branch of this library and is the source of many issues when developing applications that for any reason use a not-so-reliable connection to the nodes.

Implementing of the reconnect handling means basically resolving several major problems:

  • Reconnecting the transport layer, this is the websocket connection itself.
  • Reattaching client's event listeners that handle the different websocket connection states.
  • Resubscribing to higher-level events (subscriptions)

Those fixes do change the following:

  • The WebsocketProvider inherits now from the EventEmitter object.
  • RequestManager's provider listeners updated:
    • Adds 'error' listener (does forward the error to all subs)
    • Adds 'connect' listener (resubscribes if subscriptions do exists)
    • Adds 'close' listener (does forward the close event as an error to all subs)
  • Request queue added to WebsocketProvider
  • WebsocketProvider error handling improved.
  • WebsocketProvider socket listener handling improved
  • WebsocketProvider reconnect method implemented

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Checklist:

  • I have selected the correct base branch.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have made corresponding changes to the documentation.
  • My changes generate no new warnings.
  • Any dependent changes have been merged and published in downstream modules.
  • I ran npm run test with success and extended the tests if necessary.
  • I ran npm run build and tested the resulting file from dist folder in a browser.
  • I have tested my code on the live network.

nivida and others added 24 commits October 24, 2019 14:32
When using `.on<event>=fn` to attach listeners, only one listener can be
set at the same time. Since multiple request managers can use the same
provider, the EventTarget API has to be used to ensure all of them
receive the events emitted from the provider.

This is needed on both the `on` and `removeListener` functions.
The method `once` is required to allow the subscription logic to
identify if the provider is able to reconnect/resubscribe and then
attach to the following `connect` event the function to resubscribe.
When the subscription fails on start and when it fails after it was
successfully established, the same logic needs to be executed: remove
subscription, listen for the next `connect` event if available to
actually subscribe again, emit the error and call the callback.

Prior code did that only for established subscriptions so if a
subscription was unable to be set right on start, no resubscription was
ever tried.

The logic was moved to a single method to avoid duplication of code.
In addition reentry is avoided by checking and properly clearing the
`_reconnectIntervalId` variable.
On subscribe, if there is an existing `id`, the subscription listeners
are removed. In the case of a resubscription, the listeners have to be
kept. Therefore, the `id` property -that will change anyway- must be
cleared so the listeners are not removed.

Then, after the subscription object resubscribes, the listeners set by
the subscription user code remain untouched, making the resubscription
transparent to the user code.
When the request manager removes a subscription due to an error, it
tries to send an unsubscribe package, which can also fail if i.e. the
network is down. In such a case, the function must not allow reentry.
Removing the subscription first ensures it will not do so.

In addition, if the subscription was already removed, the callback shall
be called anyway.
When error events are emitted by the provider, all subscriptions shall
receive the event and trigger the unsubscription/resubscription logic.
By wrapping the available WebSocket implementation (native WebSocket
object or `websocket` package) with `websocket-reconnector`, the
provider is given a WebSocket that will automatically reconnect on
errors.

A new option was added to the WebSocket provider to controll whether it
should automatically reconnect or it should behave as usual.
In the case any websocket call takes too long to return and a timeout
was set for the provider to timeout, the provider should try to restart
the connection.

This could happen, for instance, if the client loses connection with the
server, the server closes the connection and later, the connectivity is
up again but since the client did not receive the closing frame *and*
the client does not attempt to send any package to the server, no error
is observed.

`websocket` implementation for Node.js has an option to send keep-alive
frames and detect such scenarios, but the standard browser W3C WebSocket
does not, so it is "vulnerable" to this kind of failure which will
mostly affect web3 subscriptions.
If the provider silently recoonects and emits a new "connect" event, the subscriptions have to be set again over that new connection.
…cribing after the connection got lost will now get triggered from the RequestManager instead of the subscriptions
@nivida
Copy link
Contributor Author

nivida commented Nov 8, 2019

Open discussion from PR #3171:
Bildschirmfoto 2019-11-08 um 11 04 53

@svanurin
Copy link

@nivida Thanks for the great work!
@cgewecke @holgerd77 Hello! Do you have any idea how far is it from merging this PR?
As I have issues with websockets missing events from Infura and considering rewrite my app or wait for this fixes...

@holgerd77
Copy link
Collaborator

@svanurin Won't make any promises here. If we get this merged next week during EthCC this will get its way into the following v1.2.7 release, I would assume to be released sometime during the next 2-4 weeks.

@svanurin
Copy link

@holgerd77 I see. Thank you and good luck with your work!

@cgewecke
Copy link
Collaborator

cgewecke commented Apr 3, 2020

@holgerd77 Rebased against latest 1.x now.

One of the "allowed failure" jobs (ganache-core's unit tests) is failing on a single test. This has already been reported/discussed in ganache-core 545 and they agree it's a bug in their test / websocket disconnection logic.

Copy link
Collaborator

@holgerd77 holgerd77 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skimmed through the code a last time for a rough consistency check, LGTM, will merge.

@holgerd77 holgerd77 merged commit cae5334 into 1.x Apr 6, 2020
@holgerd77 holgerd77 deleted the provider-related-improvements branch April 6, 2020 20:45
@wbt
Copy link
Contributor

wbt commented Apr 14, 2020

Thanks for all the work that went into this! This has been a blocker to upgrading for me, blocking being able to report and/or fix issues in the latest.
Is there any general time-frame for when things might be tested and formally released?

@cgewecke
Copy link
Collaborator

Hi @wbt. We're working on releasing 1.2.7-rc.0 today (which will include this PR).

And 1.2.7 should release next week if we don't discover any bugs while testing the rc.

@dr3adx
Copy link

dr3adx commented Apr 20, 2020

WHEN IS 1.2.7 GONNA BE RELEASED JESUS CHRIST THIS IS A MAJOR BUG

@cgewecke
Copy link
Collaborator

@dr3adx This work is published in 1.2.7-rc.0 and we should cut a formal release for that this week.

If you'd like to try it out right now you can install with

npm install --save web3@rc

@frozeman
Copy link
Contributor

@dr3adx what issues do you see?

@frozeman
Copy link
Contributor

@cgewecke do I understand this PR right and it removes the ability to give each module a different provider connection?
If so than this is a bug, as I had built it on purpose that way to show connection to different nodes / chains at the same time with the same web3 instance.

@cgewecke
Copy link
Collaborator

@frozeman, thanks for checking this over...

it removes the ability to give each module a different provider connection?

This topic was raised back in November when the change was first proposed in this thread. There it seemed like the main concern was that the swarm module preserved its own provider.

If you have a chance could you clarify what the expected behavior is with an example?

@hfa0
Copy link

hfa0 commented Apr 25, 2020

@nivida seems like the providers error and end events are not firing anymore. Instead, the contracts error event does. Is this intended behaivoir ?

provider = new Web3.providers.WebsocketProvider(url);
provider.on('end', () => {
  // not fired
});
provider.on('error', () => {
  // not fired
});
contract.events[type]({ fromBlock }).on('error', (err) => {
  // event fired
})
provider.disconnect(1012, 'restart');

@cgewecke
Copy link
Collaborator

@hfa0 Apologies, this is a bug. Have opened this as a separate issue in #3485 and will open a PR patching rn.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
1.x 1.0 related issues Enhancement Includes improvements or optimizations Feature Request Review Needed Maintainer(s) need to review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

MaxListenersExceededWarning when sending contract (v1.0.0-beta.33)