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

Websocket provider #566

Closed
banteg opened this issue Jan 19, 2018 · 43 comments
Closed

Websocket provider #566

banteg opened this issue Jan 19, 2018 · 43 comments

Comments

@banteg
Copy link
Contributor

banteg commented Jan 19, 2018

What is wrong?

Websocket have some nice benefits over HTTP based connections to a node.

  • web3.js v.1.0 deprecated HttpProvider in favor of WebsocketProvider
  • infura added public websocket endpoints
  • there is an excellent websockets library
  • websockets give 4-8x better performance compared to HTTP/IPC from my tests

How it can be fixed.

Whoever takes this on is going to be committing to dealing with figuring out some unknown unknowns. It's unclear at the time of writing this issue how much work adding this provider is going to require.

The naive implementation plan is as follows:

Implement a new class web3.providers.websocket.WebsocketProvider. This class should use the websockets library linked above to establish (or reuse an existing) websocket connection to send the JSON-RPC request and get the response.

Definition of Done

  • Documentation on the constructor of WebsocketProvider alongside the other provider documentation.
  • Documentation on how to use the WebsocketProvider including a full working example of running the event loop in a small example app.
  • Provider level tests which test the basics of the provider connecting to a locally run websocket endpoint and sending/receiving basic requests/responses.
    • These should probably use pytest-asyncio
@pipermerriam
Copy link
Member

I think it's time we have a talk about adding a websocket provider

I found this absolutely hilarious for some reason....

@pipermerriam
Copy link
Member

I suspect we can get by with a minimal initial implementation which operates synchronously much the same way the IPC provider does? I am not well versed in websockets so would need to validate this assertion.

@banteg
Copy link
Contributor Author

banteg commented Jan 19, 2018

I experimented with this a while ago, I'm afraid it can't be easily added. Websockets require a persistent connection and a running event loop. Here it gets tricky as async calls spread to the very top. Other than that, the API is the same.

The one approach I see feasible with the current design is adding an AsyncRequestManager, but I haven't dug too deep after failing to implement a quick and dirty support for it.

A quick example:

import json
import asyncio
import websockets

async def main():
    ws = await websockets.connect('wss://mainnet.infura.io/ws')
    request = dict(jsonrpc='2.0', id=1, method='eth_blockNumber', params=[])
    await ws.send(json.dumps(request))
    response = await ws.recv()
    print(json.loads(response)['result'])

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

@carver
Copy link
Collaborator

carver commented Jan 20, 2018

websockets give 4-8x better performance compared to HTTP/IPC from my tests

The performance spread between HTTP and IPC is higher than 2x in my experience. Did you separately test against IPC, or are you just bundling them together after running HTTP tests?

BTW, I'm not arguing against a WebSockets provider; it's valuable enough to implement if it's only faster than HTTP, for services that are only available remotely. But I am genuinely curious how it compares specifically to IPC.

@carver
Copy link
Collaborator

carver commented Jan 20, 2018

Websockets require a persistent connection and a running event loop.

This isn't super different from IPC. A websockets implementation could probably use something similar to IPC's PersistantSocket implementation: https://github.com/ethereum/web3.py/blob/master/web3/providers/ipc.py#L31

@owocki
Copy link

owocki commented Mar 12, 2018

@pipermerriam @carver id love to see websockets available in web3py. in fact, it looks like infura wants to see it too

is this on the roadmap soon? is it a good bounty-able issue? perhaps given the route that @pipermerriam suggests here?

I suspect we can get by with a minimal initial implementation

@pipermerriam
Copy link
Member

I'm not sure if they need to be done in conjunction with each other, but this issue compliments the websockets one.

#657

Since both need asyncio.

@pipermerriam
Copy link
Member

Main issue has been updated to reflect what I think is the minimum necessary implementation.

@gsalgado
Copy link

gsalgado commented Mar 13, 2018

AIUI, the new WebsocketProvider would just need to provide a synchronous make_request() method? One way to do that is to start a thread running loop.run_forever(), and then implement WebsocketProvider like this:

class WebsocketProvider:
    def __init__(...):
        self.loop = loop
        self.conn = websockets.client.connect(..., loop=loop)

    async def coro_make_request(...):
        await self.conn.send(...)
        res = await self.conn.recv(...)

    def make_request(...):
        future = asyncio.run_coroutine_threadsafe(self.coro_make_request(...), self.loop)
        return future.result()

@voith
Copy link
Contributor

voith commented Mar 14, 2018

I would like to work on this. I am not a asyncio user, however I have experience with twisted. I should get started on this, this weekend.

@pipermerriam @carver Would it be fine to add a third party dependency like websockets. It seems to be built on top of asyncio.

@gitcoinbot
Copy link

This issue now has a funding of 0.4 ETH (263.2 USD @ $658.0/ETH) attached to it.

  • If you would like to work on this issue you can claim it here.
  • If you've completed this issue and want to claim the bounty you can do so here
  • Questions? Get help on the Gitcoin Slack
  • $3852.92 more Funded OSS Work Available at: https://gitcoin.co/explorer

@gitcoinbot
Copy link

gitcoinbot commented Mar 14, 2018

Work has been started on the 0.4 ETH (168.93 USD @ $422.33/ETH) funding by:

  1. @voith

Please work together and coordinate delivery of the issue scope. Gitcoin doesn't know enough about everyones skillsets / free time to say who should work on what, but we trust that the community is smart and well-intentioned enough to work together. As a general rule; if you start work first, youll be at the top of the above list ^^, and should have 'dibs' as long as you follow through.

On the above list? Please leave a comment to let the funder (@owocki) and the other parties involved what you're working, with respect to this issue and your plans to resolve it. If you don't leave a comment, the funder may expire your submission at their discretion.

@pipermerriam
Copy link
Member

@voith take a look at the main issue description. It lays out some basics for how this should be implemented and tested (and suggests using the websockets python library to answer your question explicitely).

Looking forward to seeing this get started. Please get a pull request open this weekend as soon as you've started your work (we'll mark it as a work in progress).

@boneyard93501
Copy link
Contributor

@voith i was looking at this at one point but then got totally distracted with connection pools (e.g., ws4py ) and autobahn's client factory. also, rust-web3 has an interesting take on channels. below a very rudimentary and self-contained (but wasteful) approach using futures. maybe it's of some value to you.

#!/usr/bin/env python3
# -*- coding: utf8 -*-
import os
import json
import asyncio
import websockets


from web3.providers.base import (
    JSONBaseProvider,
)


def get_default_endpoint():
    return os.environ.get('WEB3_WS_PROVIDER_URI', 'ws://locahost:8546')


class WebsocketProvider(JSONBaseProvider):
    # don't think we need to provide request kwargs here
    loop = None

    # consider the possbility of passing the loop
    def __init__(self, endpoint_uri=None):
        self.endpoint_uri = endpoint_uri
        if self.endpoint_uri is None:
            self.endpoint_uri = get_default_endpoint()
        if self.loop is None:
            self.loop = asyncio.get_event_loop()
        super().__init__()

    def __str__(self):
        return "WS connection {0}".format(self.endpoint_uri)

    def __del__(self):
        # self.loop.close()  # ha,ha think async!!
        pass

    def _is_ws_connected(self):
        # don't need it if we're doing the eth_protocolVersion call to check
        # and don't use a connection pool manager. but if we do, make it async
        raise NotImplementedError()

    def _close_ws_connection(self):
        # don't need it if we use the context manager but if we do,
        # make it async
        raise NotImplementedError()

    async def rpc_call(self, request_data, future):
        # assert request_data
        async with websockets.connect(self.endpoint_uri) as ws:
            await ws.send(request_data)
            result = await ws.recv()
        future.set_result(result)

    def make_request(self, request_data):
    # def make_request(self, method, params):
        # request_data = self.encode_rpc_request(method, params)
        loop = self.loop
        future = asyncio.Future()
        asyncio.ensure_future(self.rpc_call(request_data, future))
        loop.run_until_complete(future)
        result = future.result()
        return json.loads(result)


def lame_test(net_id='3', proto_ver='63', syncing=False):
    call_q = [({"jsonrpc": "2.0", "method": "eth_protocolVersion", "params": [], "id": 1}, proto_ver),
              ({"jsonrpc": "2.0", "method": "eth_syncing", "params": [], "id": 2}, syncing),
              ({"jsonrpc": "2.0", "method": "net_version", "params": [], "id": 3}, net_id)
              ]

    uri = 'ws://127.0.0.1:8546'

    for payload in call_q:
        WSP = WebsocketProvider(endpoint_uri=uri)
        assert WSP.make_request(json.dumps(payload[0]))['result'] == payload[1]


    WSP = WebsocketProvider(endpoint_uri=uri)
    for payload in call_q:
        assert WSP.make_request(json.dumps(payload[0]))['result'] == payload[1]


if __name__ == '__main__':
    lame_test()

the handling of the event loop(s) and the possible reuse/keep alive of the connections should probably viewed in light of the overall async strategy. regardless, a ws connection pool is imo the way to go. bon chance.

@voith
Copy link
Contributor

voith commented Mar 18, 2018

STATUS UPDATE:
I've managed to get working a basic version of the WebsocketProvider which includes @gsalgado's suggestion of starting the provider in a thread as discussed in the gitter chatroom.

Provider level tests which test the basics of the provider connecting to a locally run websocket endpoint and sending/receiving basic requests/responses.

@pipermerriam I tried looking if similar tests have been written for other providers but I couldn't find any. If such tests exists, can you point me to the code? I've had a look at the unit tests for the providers and also the EthereumTesterProvider. I'm guessing this is more like an integrated test. If such tests don't exist, I wouldn't mind writing them. Its just that I don't want to duplicate work.

UPDATE: Found the tests

@voith
Copy link
Contributor

voith commented Mar 18, 2018

Also, I have tested some basic methods manually.
However I am getting an error with getBlock. I'll dig into this later. But posting it here in order to get some feedback .
I am using geth 1.8.2-stable-b8b9f7f4 which is connected to rinkeby

In [13]: web3.eth.getBlock(1)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/Projects/web3.py/web3/utils/formatters.py in apply_formatters_to_dict(formatters, value)
     66             try:
---> 67                 yield key, formatters[key](item)
     68             except (TypeError, ValueError) as exc:

~/Python-Env/web3-dev/lib/python3.6/site-packages/cytoolz-0.9.0.1-py3.6-macosx-10.12-x86_64.egg/cytoolz/functoolz.pyx in cytoolz.functoolz.curry.__call__()

~/Projects/web3.py/web3/middleware/pythonic.py in to_hexbytes(num_bytes, val, variable_length)
     80             "The value %r is %d bytes, but should be %d" % (
---> 81                 result, len(result), num_bytes
     82             )

ValueError: The value HexBytes('0xd783010602846765746887676f312e372e33856c696e75780000000000000000398ef9849cd036a7d8bffc2c9b39a43f23e1c760f2294a7e6bdec559585f91bf4027911d0c801839611d801e8a92e6ce8042713e7cb69c99095f340dcf6ae51a00') is 97 bytes, but should be 32

The above exception was the direct cause of the following exception:

ValueError                                Traceback (most recent call last)
<ipython-input-4-fad7d258f672> in <module>()
----> 1 web3.eth.getBlock(200000)

~/Projects/web3.py/web3/eth.py in getBlock(self, block_identifier, full_transactions)
    146         return self.web3.manager.request_blocking(
    147             method,
--> 148             [block_identifier, full_transactions],
    149         )
    150

~/Projects/web3.py/web3/manager.py in request_blocking(self, method, params)
    101         Make a synchronous request using the provider
    102         """
--> 103         response = self._make_request(method, params)
    104
    105         if "error" in response:

~/Projects/web3.py/web3/manager.py in _make_request(self, method, params)
     84             request_func = provider.request_func(self.web3, tuple(self.middleware_stack))
     85             try:
---> 86                 return request_func(method, params)
     87             except CannotHandleRequest:
     88                 continue

~/Projects/web3.py/web3/middleware/gas_price_strategy.py in middleware(method, params)
     16                     transaction = assoc(transaction, 'gasPrice', generated_gas_price)
     17                     return make_request(method, [transaction])
---> 18         return make_request(method, params)
     19     return middleware

~/Projects/web3.py/web3/middleware/formatting.py in middleware(method, params)
     21                 response = make_request(method, formatted_params)
     22             else:
---> 23                 response = make_request(method, params)
     24
     25             if 'result' in response and method in result_formatters:

~/Projects/web3.py/web3/middleware/attrdict.py in middleware(method, params)
     16     """
     17     def middleware(method, params):
---> 18         response = make_request(method, params)
     19
     20         if 'result' in response:

~/Projects/web3.py/web3/middleware/formatting.py in middleware(method, params)
     28                     response,
     29                     'result',
---> 30                     formatter(response['result']),
     31                 )
     32                 return formatted_response

~/Python-Env/web3-dev/lib/python3.6/site-packages/cytoolz-0.9.0.1-py3.6-macosx-10.12-x86_64.egg/cytoolz/functoolz.pyx in cytoolz.functoolz.curry.__call__()

~/Projects/web3.py/web3/utils/formatters.py in apply_formatter_if(condition, formatter, value)
     54 def apply_formatter_if(condition, formatter, value):
     55     if condition(value):
---> 56         return formatter(value)
     57     else:
     58         return value

~/Python-Env/web3-dev/lib/python3.6/site-packages/cytoolz-0.9.0.1-py3.6-macosx-10.12-x86_64.egg/cytoolz/functoolz.pyx in cytoolz.functoolz.curry.__call__()

~/Python-Env/web3-dev/lib/python3.6/site-packages/eth_utils-1.0.1-py3.6.egg/eth_utils/functional.py in inner(*args, **kwargs)
     20         @functools.wraps(fn)
     21         def inner(*args, **kwargs):
---> 22             return callback(fn(*args, **kwargs))
     23
     24         return inner

~/Projects/web3.py/web3/utils/formatters.py in apply_formatters_to_dict(formatters, value)
     67                 yield key, formatters[key](item)
     68             except (TypeError, ValueError) as exc:
---> 69                 raise type(exc)("Could not format value %r as field %r" % (item, key)) from exc
     70         else:
     71             yield key, item

ValueError: Could not format value '0xd783010602846765746887676f312e372e33856c696e75780000000000000000398ef9849cd036a7d8bffc2c9b39a43f23e1c760f2294a7e6bdec559585f91bf4027911d0c801839611d801e8a92e6ce8042713e7cb69c99095f340dcf6ae51a00' as field 'extraData'

This is the response returned:

(Pdb) json.loads(res)
{'jsonrpc': '2.0', 'id': 0, 'result': {'difficulty': '0x2', 'extraData': '0xd783010602846765746887676f312e372e33856c696e75780000000000000000398ef9849cd036a7d8bffc2c9b39a43f23e1c760f2294a7e6bdec559585f91bf4027911d0c801839611d801e8a92e6ce8042713e7cb69c99095f340dcf6ae51a00', 'gasLimit': '0x47f9bc', 'gasUsed': '0x0', 'hash': '0x2f17dff3847d8eda807e6cdb3113e6b54c3a0af1bdc54ac3bac2ce20e749c468', 'logsBloom': '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', 'miner': '0x0000000000000000000000000000000000000000', 'mixHash': '0x0000000000000000000000000000000000000000000000000000000000000000', 'nonce': '0x0000000000000000', 'number': '0x30d40', 'parentHash': '0x1ed149914fe0b79bf68fba82774b634ed7892ccf0b6fa2c1229f4f2ba7fef305', 'receiptsRoot': '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', 'sha3Uncles': '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', 'size': '0x261', 'stateRoot': '0x5cc70fe0dc1a8c0b8e7b6dc4bf504a0aee8fbf314de4b92fb14cb3febd52c16e', 'timestamp': '0x591c0cb7', 'totalDifficulty': '0x5f595', 'transactions': [], 'transactionsRoot': '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', 'uncles': []}}

@voith
Copy link
Contributor

voith commented Mar 18, 2018

never mind, found the tests!

@pipermerriam
Copy link
Member

pipermerriam commented Mar 18, 2018

@voith You should get a pull request open and we can start from there.

@pipermerriam
Copy link
Member

@voith
Copy link
Contributor

voith commented Mar 19, 2018

@pipermerriam Thanks! The middleware fixed the issue.
I have managed to get tests running with parity. I will open a PR tonight(IST), just have some cleaning up to do.

@pipermerriam
Copy link
Member

Our preference is for you to open the PR as soon as you have code. Pull requests do not need to be polished or done. This can save you a lot of time since that allows us to have a look at your implementation and give feedback/direction earlier on.

@voith voith mentioned this issue Mar 19, 2018
2 tasks
@voith
Copy link
Contributor

voith commented Mar 23, 2018

I'm posting the design decision held between @pipermerriam and @gsalgado on gitter, to keep track.
screen shot 2018-03-15 at 12 30 38 am
screen shot 2018-03-15 at 12 31 10 am

@voith
Copy link
Contributor

voith commented Mar 23, 2018

while trying to address #708 (comment), I came up with the following solution:
Posting this here because this is more of a design decision and the issue seems to be a better place than the PR.

import asyncio
import websockets
from threading import (
    current_thread,
    Thread,
)

from web3.providers.base import (
    JSONBaseProvider,
)


THREAD_POLLING_INTERVAL = 2  # secs


async def _stop_loop(loop, parent_thread):
    while parent_thread.is_alive():
        await asyncio.sleep(THREAD_POLLING_INTERVAL)
    else:
        loop.stop()


def _start_event_loop(loop, parent_thread):
    asyncio.set_event_loop(loop)
    asyncio.run_coroutine_threadsafe(_stop_loop(loop, parent_thread), loop)
    loop.run_forever()
    loop.close()


def _get_threaded_loop():
    new_loop = asyncio.new_event_loop()
    thread_loop = Thread(target=_start_event_loop, args=(new_loop, current_thread()))
    thread_loop.start()
    return new_loop

class WebsocketProvider(JSONBaseProvider):

    _loop = None

    def __init__(self, endpoint_uri=None):
        if self._loop is None:
            self._loop = _get_threaded_loop()
        ...

The solution has been inspired by: https://stackoverflow.com/a/18643557/3526700

Since the event_loop runs in a separate thread, the problem I was having was to figure out a clean way to exit the program whenever the main thread would exit. The main culprit here is loop.run_forever(). Since this is a never ending loop, the thread_loop would never exit. So the solution I came up was to stop the loop whenever the thread that created it, exited. However, In order to do so I had to poll the parent_thread to check if it was alive. I don't like the polling solution, but its a coroutine that runs async, so Its not very bad.

@pipermerriam @carver Can you think of a better solution? Or is this good for now.

@carver
Copy link
Collaborator

carver commented Mar 23, 2018

This seems okay, probably. I also wish for there to be an easier way. But sometimes with threading, it's tough to find one. Just to ask the question. Could we just get away with putting the asyncio loop on a Thread with (..., daemon=True)? What bad state could that put us in?

@voith
Copy link
Contributor

voith commented Mar 24, 2018

Could we just get away with putting the asyncio loop on a Thread with (..., daemon=True)? What bad state could that put us in?

I am not an expert in threading, however from what I've understood daemon threads are used to run background tasks that are only useful when the main program runs. In this case, loop.run_forever() seems to be a good candidate for daemon threads.
@pipermerriam Is there any reason why you think that we shouldn't use a daemon thread in this case?

@boneyard93501
Copy link
Contributor

when i looked at it, approaching it from the perspective of self-contained event loops seemed to make the most sense given the existing sync architecture. from a think-async perspective not optimal but given the code base, it's a pretty good approach. wrapping each loop in it's own thread, as suggested by @carver, gives you also the basis for the socket registration pool i eluded to earlier and a guaranteed cleanup on exit. fundamentally, it's a gevent approach without the covers.

@boneyard93501
Copy link
Contributor

@voith while demonized cleans up threads, some of the (IO) resources may be locked for a bit. generally not for very long but if it's a concern, using threading.Event in a graceful exit method with non-demonized threads would alleviate that issue. you probably wan to cascade closing each ws con for thread alive.

@voith
Copy link
Contributor

voith commented Mar 25, 2018

@boneyard93501 Thanks for weighing in!

Event in a graceful exit method with non-demonized threads would alleviate that issue. you probably wan to cascade closing each ws con for thread alive.

You're absolutely right. I wasn't having any problems till now as I was creating and closing a new websocket connection for every request. The moment I implemented a Persitent connection, I'm getting some spurious errors.

class PersistentWebSocket:

    def __init__(self, endpoint_uri, loop):
        self.ws = None
        self.endpoint_uri = endpoint_uri
        self.loop = loop

    def __del__(self):
        if self.ws:
            self.loop.run_until_complete(self.ws.close())

    async def __aenter__(self):
        if self.ws is None:
            self.ws = await websockets.connect(uri=self.endpoint_uri, loop=self.loop)
        return self.ws

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if exc_val is not None:
            try:
                self.ws.close()
            except Exception:
                pass
            self.ws = None

Now I get this error:

task: <Task pending coro=<WebSocketCommonProtocol.transfer_data() running at /Users/voith/Python-Env/web3-test/lib/python3.6/site-packages/websockets/protocol.py:496> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x10ac87378>()]> cb=[<TaskWakeupMethWrapper object at 0x10ac87528>()]>
Task was destroyed but it is pending!
task: <Task pending coro=<WebSocketCommonProtocol.close_connection() running at /Users/voith/Python-Env/web3-test/lib/python3.6/site-packages/websockets/protocol.py:711> wait_for=<Task pending coro=<WebSocketCommonProtocol.transfer_data() running at /Users/voith/Python-Env/web3-test/lib/python3.6/site-packages/websockets/protocol.py:496> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x10ac87378>()]> cb=[<TaskWakeupMethWrapper object at 0x10ac87528>()]>>
Exception ignored in: <generator object WebSocketCommonProtocol.close_connection at 0x10ac78a98>
Traceback (most recent call last):
  File "/Users/voith/Python-Env/web3-test/lib/python3.6/site-packages/websockets/protocol.py", line 741, in close_connection
    self.writer.close()
  File "/usr/local/Cellar/python3/3.6.4_2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/streams.py", line 312, in close
    return self._transport.close()
  File "/usr/local/Cellar/python3/3.6.4_2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/selector_events.py", line 621, in close
    self._loop.call_soon(self._call_connection_lost, None)
  File "/usr/local/Cellar/python3/3.6.4_2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py", line 574, in call_soon
    self._check_closed()
  File "/usr/local/Cellar/python3/3.6.4_2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py", line 357, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed

The problem here is that the event loop is stopped before __del__ is called.

I also tried to wait for the pending tasks to finish:

async def _stop_loop(loop, parent_thread):
    while parent_thread.is_alive():
        await asyncio.sleep(THREAD_POLLING_INTERVAL)
    else:
        all_tasks = asyncio.Task.all_tasks(loop)
        current_task = asyncio.Task.current_task(loop)
        pending_tasks = all_tasks - {current_task}
        loop.run_until_complete(asyncio.gather(*pending_tasks))
        loop.stop()

But this waits forever. garabage collection doesn't work as I expected it to work.

However, I don't get this problem with daemon=True or atleast it doesn't complain

@pipermerriam
Copy link
Member

Looks like using daemon=True is the best solution on the table? Am I missing anything?

@boneyard93501
Copy link
Contributor

boneyard93501 commented Mar 25, 2018

@voith @pipermerriam i don't think you want to treat ws provider analogous to httpprovider, which means reuse and requires some additional management at the request manager level. otherwise you may end up not only blocking a lot of ports on the clients-side but possibly bring the ws server on the node to grinding halt. not likely but plausible especially in a multi-user per node situation.

let's say you have a (global) deque, called ws_registry, possibly pre-seeded with, say, five ws connections at startup (as part of the Web3(WSProvider(...)) init), and the modified make_request procedure pseudo-looks like so:

if provider_type is websocket
    if ws available
        ws = ws_registry.popleft() 
    else
        ws = make_ws_connection
    execute request using ws
    ws_registry.pushleft(ws)

you ought to get away with ws clients based on a slightly modified websocket echo client example which is reasonably clean.

add ws_registry.pushleft(ws) to the on_open and ws_registry.remove(ws) to the on_close, i think you're mostly covered. it's still far from ideal but imo you end up reusing connections getting the real ws benefits.
if you then add a background task that sweeps ws_registry to keep-alive at least k, e.g., five, connections, remove the ones the server closed due to inactivity, and, if you want to be fancy, limit the "idle" number of connections on ws_registry to, say, 3*k, using something like [ws.close() for ws in ws_registry[-1:-j]] you should be ok ... i think.

@voith
Copy link
Contributor

voith commented Mar 25, 2018

Looks like using daemon=True is the best solution on the table? Am I missing anything?

from what I've explored, daemon=True looks like the best way to me. However, I will try to dig in a bit more in the source code of libraries like gevent. If you know any other libraries that use coroutines and threads then I'd be happy to explore them too.

@boneyard93501 If I understand you correctly, I think you are worried about the number of connections that will be opened up in a multi websocket provider situation and the solution you're proposing is to use a global ConectionPool? I don't know if that's what is desired in the current scope of adding this feature. Let @pipermerriam and @carver take this call and then I can explore if needed!

add ws_registry.pushleft(ws) to the on_open and ws_registry.remove(ws) to the on_close

I don't think the websockets library that we are using here offers such events. It is designed to work with coroutines and not callbacks. I don't know how much work it will take to implement a ConnectionPool using the websockets library.

@pipermerriam
Copy link
Member

Lets assume connection pooling is out of scope for this initial implementation. I agree that it may be an issue, but I want to get the core functionality into the main codebase before we start optimizing things.

@voith
Copy link
Contributor

voith commented Mar 26, 2018

Will work on this tomorrow. I won't be available this weekend, so I'll try my best to finish this before Thursday.

@gitcoinbot
Copy link

Work for 0.4 ETH (149.73 USD @ $374.31/ETH) has been submitted by:

  1. @voith

Submitters, please leave a comment to let the funder (@owocki) (and the other parties involved) that you've submitted you work. If you don't leave a comment, the funder may expire your submission at their discretion.

@voith
Copy link
Contributor

voith commented Apr 4, 2018

@owocki I have completed this task via #708

@owocki
Copy link

owocki commented Apr 4, 2018

@carver @pipermerriam 👌 to payout?

@pipermerriam
Copy link
Member

delegating to @carver

@carver
Copy link
Collaborator

carver commented Apr 4, 2018

Good to go 👍

@carver carver closed this as completed Apr 4, 2018
@gitcoinbot
Copy link

The funding of 0.4 ETH (152.43 USD @ $381.06/ETH) attached to this issue has been approved & issued to @voith.

@voith
Copy link
Contributor

voith commented Apr 5, 2018

Thanks guys, I have received the bounty :)

@dangell7
Copy link

dangell7 commented May 2, 2018

I used threading with Deamons at first and then I switched to celery to deploy and transact with functions async. I had no experience with asyncio so that’s the decision I chose. I know it prob doesn’t matter, I just wanted to give my input on how I solved it in our production application.

@voith
Copy link
Contributor

voith commented May 2, 2018

Hi @dangell7 Good to know!
I think asyncio is best suited for WebsocketProvider as communicating with the node is IO bound. I normally use celery when I want to run background jobs usually running CPU intensive tasks. Also there's an overhead to maintain/deploy your celery workers. web3.py provides a simple interface for users without having to bother about handling the event loop.

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

No branches or pull requests

10 participants