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

Config/Settings to ApplicationRunner (openHandshakeTimeout) #838

Closed
TonyRehley opened this issue May 15, 2017 · 35 comments · Fixed by #912
Closed

Config/Settings to ApplicationRunner (openHandshakeTimeout) #838

TonyRehley opened this issue May 15, 2017 · 35 comments · Fixed by #912

Comments

@TonyRehley
Copy link

TonyRehley commented May 15, 2017

Hi,
How can I override default openHandshakeTimeout param?
I am going run ApplicationRunner with greater handshake timout in python code.
I have tried, but nothing:
subscriber = ApplicationRunner(url=u"wss://api.....", realm=u"realm1",extra={'openHandshakeTimeout':10})

@jonathanstrong
Copy link

+1 struggling to find where to change this as well.

@TonyRehley
Copy link
Author

Some here some developers to answer?

@oberstet
Copy link
Contributor

This isn't currently exposed at the ApplicationRunner level (only on the underlying WebSocketFactory level).

@michael-wise
Copy link

To clarify, there is currently no access via ApplicationSession or ApplicationRunner? Is there a single place to hard-patch at the module level?

@oberstet
Copy link
Contributor

oberstet commented Jun 2, 2017

@michael-wise monkey patching will not work, as the underlying factory is a locally scoped variable: https://github.com/crossbario/autobahn-python/blob/master/autobahn/twisted/wamp.py#L256

@myth
Copy link
Contributor

myth commented Jun 8, 2017

I'm kinda stuck waiting for this fix to be able to work with a server under heavy load that usually takes longer to complete handshake.

The hard-coded values in https://github.com/crossbario/autobahn-python/blob/master/autobahn/twisted/wamp.py#L256 would still override the fix in #844? Would love it if you would correct that before doing the next release.

I tried using the head of master after #844, and passing the options to the extra kwarg in the ApplicationRunner, which I assume should get passed as these new component options, but to no avail.

@oberstet
Copy link
Contributor

oberstet commented Jun 8, 2017

@myth curious, what server exactly? could you describe your setup?

@myth
Copy link
Contributor

myth commented Jun 8, 2017

Hi and thanks for the fast reply!

I'm attempting to connect to wss://api.poloniex.com, a cryptocurrency exchange, to get the ticker and orderbook feeds. I assume they are regularly DDoSed, but they are behind cloudflare.

I'm just prototyping the application for now, hence using the ApplicationRunner convenience, but my setup is just:

class Poloniex(ApplicationSession):
    def __init__(self, *args, **kwargs):
        ApplicationSession.__init__(self, *args, **kwargs)
        self.logger = getLogger(__name__)

    def on_event(self, *args):
        self.logger.info('{0}: {1} A:{2} B:{3} {4}% V:{5} H:{8} L:{9}'.format(*args))

    @inlineCallbacks
    def onJoin(self, details):
        yield self.subscribe(self.on_event, 'ticker')

And i start using the runner:

component_runner = ApplicationRunner(
    'wss://api.poloniex.com',
    'realm1',
    extra={'openHandshakeTimeout': 15, 'closeHandshakeTimeout': 15}
)
component_runner.run(Poloniex)

To figure out what I needed to set the timeout to to make sure the connection would finish before being preemtively terminated, I patched the hardcoded numbers in https://github.com/crossbario/autobahn-python/blob/master/autobahn/twisted/wamp.py#L256 and set both to 60 just to test. I also had to increase the ping timeout, as that also happened a couple of times.

This is the debug output from the runner, where my attempt at passing in an option dict to the extra kwarg with timeouts of 15s respectively, has no effect.

2017-06-08T14:45:50+0200 
[('logOctets', False, 'WampWebSocketClientFactory'),
 ('logFrames', False, 'WampWebSocketClientFactory'),
 ('trackTimings', False, 'WampWebSocketClientFactory'),
 ('utf8validateIncoming', True, 'WampWebSocketClientFactory'),
 ('applyMask', True, 'WampWebSocketClientFactory'),
 ('maxFramePayloadSize', 1048576, 'WampWebSocketClientFactory'),
 ('maxMessagePayloadSize', 1048576, 'WampWebSocketClientFactory'),
 ('autoFragmentSize', 65536, 'WampWebSocketClientFactory'),
 ('failByDrop', False, 'WampWebSocketClientFactory'),
 ('echoCloseCodeReason', False, 'WampWebSocketClientFactory'),
 ('openHandshakeTimeout', 60.0, 'WampWebSocketClientFactory'),
 ('closeHandshakeTimeout', 60.0, 'WampWebSocketClientFactory'),
 ('tcpNoDelay', True, 'WampWebSocketClientFactory'),
 ('autoPingInterval', 5., 'WampWebSocketClientFactory'),
 ('autoPingTimeout', 10.0, 'WampWebSocketClientFactory'),
 ('autoPingSize', 4, 'WampWebSocketClientFactory'),
 ('version', 18, 'WampWebSocketClientFactory'),
 ('acceptMaskedServerFrames', False, 'WampWebSocketClientFactory'),
 ('maskClientFrames', True, 'WampWebSocketClientFactory'),
 ('serverConnectionDropTimeout', 1, 'WampWebSocketClientFactory'),
 ('perMessageCompressionOffers',
  [PerMessageDeflateOffer(accept_no_context_takeover = True, accept_max_window_bits = True, request_no_context_takeover = False, request_max_window_bits = 0)],
  'WampWebSocketClientFactory'),
 ('perMessageCompressionAccept',
  <function ApplicationRunner.run.<locals>.accept at 0x7f8f07996d08>,
  'WampWebSocketClientFactory')]
2017-06-08T14:45:50+0200 connection to tcp4:104.20.12.48:443 established
2017-06-08T14:45:50+0200 GET / HTTP/1.1
User-Agent: AutobahnPython/17.5.1
Host: api.poloniex.com:443
Upgrade: WebSocket
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Sec-WebSocket-Key: *removed*
Sec-WebSocket-Protocol: wamp.2.json.batched,wamp.2.json
Sec-WebSocket-Extensions: permessage-deflate; client_no_context_takeover; client_max_window_bits
Sec-WebSocket-Version: 13


2017-06-08T14:45:50+0200 Connection made to tcp4:104.20.12.48:443
2017-06-08T14:46:18+0200 received HTTP response:

b'HTTP/1.1 101 Switching Protocols\r\nDate: Thu, 08 Jun 2017 12:46:18 GMT\r\nConnection: upgrade\r\nSet-Cookie: __cfduid=*removed*; expires=Fri, 08-Jun-18 12:45:50 GMT; path=/; domain=.poloniex.com; HttpOnly\r\nX-Powered-By: AutobahnPython/0.13.0\r\nUpgrade: WebSocket\r\nSec-WebSocket-Protocol: wamp.2.json.batched\r\nSec-WebSocket-Accept: *removed*=\r\nServer: cloudflare-nginx\r\nCF-RAY: *removed*\r\n\r\n'


2017-06-08T14:46:18+0200 received HTTP status line in opening handshake : HTTP/1.1 101 Switching Protocols
2017-06-08T14:46:18+0200 received HTTP headers in opening handshake : {'set-cookie': '__cfduid=*removed*; expires=Fri, 08-Jun-18 12:45:50 GMT; path=/; domain=.poloniex.com; HttpOnly', 'upgrade': 'WebSocket', 'sec-websocket-accept': '*removed*', 'server': 'cloudflare-nginx', 'date': 'Thu, 08 Jun 2017 12:46:18 GMT', 'connection': 'upgrade', 'cf-ray': '*removed*', 'x-powered-by': 'AutobahnPython/0.13.0', 'sec-websocket-protocol': 'wamp.2.json.batched'}
2017-06-08T14:46:18+0200 openHandshakeTimeoutCall.cancel
2017-06-08T14:46:28+0200 Auto ping/pong: sending ping auto-ping/pong
2017-06-08T14:46:28+0200 Expecting ping in 10.0 seconds for auto-ping/pong
2017-06-08T14:46:28+0200 WebSocketProtocol.onPing(payload=<4 bytes>)
2017-06-08 14:46:28,165     INFO     MAIN BTC_BTS: 0.00005360 A:0.00005360 B:0.00005337 0.32738979% V:6480.87132205 H:0.00005499 L:0.00003708

As you can see, the handshaking took a very long time, 28s in this case. It varies greatly, and sometimes it will connect just after a few seconds. I'm unaware of what is the root cause of these huge swings in delay, but a means to control the protocol parameters here is required to make it work...

@oberstet
Copy link
Contributor

oberstet commented Jun 8, 2017

Interesting! They must be running a quite old version of Crossbar.io, which you can tell from the WebSocket upgrade header: 'x-powered-by': 'AutobahnPython/0.13.0'.

We (Crossbar.io GmbH) are offering commercial support for Crossbar.io and all Autobahn client libraries, and as they are running a commercial venture, we'd be happy to address these issues under a support contract. We currently are working with a couple of companies already, and because of limited resources, we prioritize paying customers.

@myth
Copy link
Contributor

myth commented Jun 8, 2017 via email

@jonathanstrong
Copy link

If I may add an unsolicited opinion from someone who's knowledge of this library extends only to what was necessary to handle the poloniex feed:

Groking the code to change a timeout parameter should not be this hard. The heavy use of OOP here makes it very difficult to understand what the code base is doing if you didn't write it. Generally speaking if you're writing automated trading systems you can RTFM. I've rarely encountered a code base that was as difficult to parse as this one.

@meejah
Copy link
Contributor

meejah commented Jun 8, 2017

Have you looked at the (in-progress) Component API? On master this allows changing the handshake timeout, too. It only currently works with Twisted (not yet asyncio) but of course that is planned.

For your use-case that would look something like this:

from autobahn.twisted.component import Component, run

component = Component(
    transports=[
        "type": "websocket",
        "url": u"wss://api.poloniex.com",
        "options": {
            "open_handshake_timeout": 60.0,
        }
    ],
    realm=u'realm1',
    # authentication= {...} if you need too
)

def on_event(*args):
    print('{0}: {1} A:{2} B:{3} {4}% V:{5} H:{8} L:{9}'.format(*args))

@component.on_join
@inlineCallbacks
def join(session, details):
    print("Session {} joined: {}".format(details.session, details))
    yield session.subscribe(on_event, 'ticker')

if __name__ == '__main__':
    run(component)

Is this API easier to understand? Although you can still use an ApplicationSession subclass if you want, there's no need to subclass to use it...

@meejah
Copy link
Contributor

meejah commented Jun 8, 2017

Note you can also do @component.subscribe(...) on the on_event function if you prefer.

@myth
Copy link
Contributor

myth commented Jun 8, 2017

@meejah Thanks, i'll look into that!

I was probably going to try to replicate the basics of what ApplicationRunner does, but was unsure of how much was needed. That example looks simple enough to get me started working with the Component API!

@oberstet
Copy link
Contributor

oberstet commented Jun 8, 2017

@jonathanstrong there are 3 main reasons for the relative complexity of the code base:

  1. we run on pretty much any combination of python version X networking framework (Twisted/asyncio)
  2. you can write app code that will run unmodified on any of 1.
  3. the library is able to "digest" broken WebSocket peers, because it is the basis for Autobahn testsuite

There are more reasons:

  1. Autobahn supports an insane number of transport combinations for WAMP. We can talk WAMP over WebSocket, RawSocket, HTTP Longpoll running over any streaming bytes transport like TCP, TLS, Unix domain socket, serial, pipes, ... and all of the former using different serializers JSON, MessagePack, CBOR, UBJSON.

The code is very well factored to allow that. Eg adding another serializer is trivial. But the price is a relative complexity in the classes involved, and how they interact.

@meejah
Copy link
Contributor

meejah commented Jun 8, 2017

The simple answer is that you shouldn't need to delve into the code just to change an option.

This is addressed by the "component" API, but is a shortcoming of the ApplicationRunner API. It should have a way to set the websocket options, but doesn't currently. This runner was originally intended as a quick way to try things out or otherwise run pieces without configuring them inside Crossbar and has slowly acquired more functionality -- but doesn't do everything.

If you are interested in making ApplicationRunner better, pull-requests are of course welcome! The "run" function from the component API can hopefully give some hints as to where to tie in to set the websocket options if you want to try this :)

Ping me in IRC (#autobahn) if you need some help

@myth
Copy link
Contributor

myth commented Jun 8, 2017

@meejah I tried your example using the following setup, but keep getting some errors:

_push_api = Component(
    realm='poloniex',
    transports=[
        {
            'url': 'wss://api.poloniex.com',
            'type': 'websocket',
            'options': {
                'auto_ping_interval': 10.0,
                'auto_ping_timeout': 10.0,
                'open_handshake_timeout': 30.0,
                'close_handshake_timeout': 30.0,
            }
        }
    ],
)

@_push_api.subscribe('ticker')
@inlineCallbacks
def on_ticker(self, *args):
    logger.info('{0}: {1} A:{2} B:{3} {4}% V:{5} H:{8} L:{9}'.format(*args))

if __name__ == '__main__':
    run(_push_api)

Errors:

2017-06-08T23:45:40+0200 connecting once using transport type "websocket" over endpoint "tcp"
2017-06-08T23:45:40+0200 component failed: RuntimeError: unknown type <class 'dict'> for "tls" configuration in transport
2017-06-08T23:45:40+0200 Connection failed: RuntimeError: unknown type <class 'dict'> for "tls" configuration in transport

I've tried looking at the source for the Component class, but can't seem to find exactly where this hiccup is coming from.

EDIT (Added trace):

--- <exception caught here> ---
  File "*removed*/env/lib/python3.5/site-packages/autobahn/twisted/component.py", line 346, in start
    yield self._connect_once(reactor, transport)
  File "*removed*/env/lib/python3.5/site-packages/autobahn/wamp/component.py", line 554, in _connect_once
    d = self._connect_transport(reactor, transport, create_session)
  File "*removed*/env/lib/python3.5/site-packages/autobahn/twisted/component.py", line 297, in _connect_transport
    transport_endpoint = _create_transport_endpoint(reactor, transport.endpoint)
  File "*removed*/env/lib/python3.5/site-packages/autobahn/twisted/component.py", line 218, in _create_transport_endpoint
    raise RuntimeError('unknown type {} for "tls" configuration in transport'.format(type(tls)))
builtins.RuntimeError: unknown type <class 'dict'> for "tls" configuration in transport

@meejah
Copy link
Contributor

meejah commented Jun 8, 2017

Ah, thanks! See #848

Meantime, you should be able to just put "tls": true alongside "type" to work around this bug.

@myth
Copy link
Contributor

myth commented Jun 8, 2017

@meejah Ty for the fast response, really appreciate it!

@myth
Copy link
Contributor

myth commented Jun 8, 2017

The 'tls': True option did not work, just spits out:

 File "*removed*/env/lib/python3.5/site-packages/autobahn/twisted/component.py", line 180, in _create_transport_factory
    "Unknown {} transport option: {}={}".format(transport.type, k, v)
builtins.ValueError: Unknown websocket transport option: tls=True

I also tried constructing the endpoint dict manually, like it is done here: https://github.com/crossbario/autobahn-python/blob/master/autobahn/wamp/component.py#L148 but that just triggered the same error as in #848

@meejah
Copy link
Contributor

meejah commented Jun 9, 2017

Sorry, the tls thing goes at the same level as options, not inside the options.

@subiol
Copy link

subiol commented Jun 16, 2017

@meejah is there an aproximate date when we can expect the component API to work with asyncio? I am suffering the same problem and my program is written all in asyncio.

@oberstet
Copy link
Contributor

@subiol pls see my comment here #838 (comment)

@subiol
Copy link

subiol commented Jun 16, 2017

@oberstet Sure, I understand, but as far as I can see this is also an open source project, so I dont see how asking for an approximate date when an API in development will be ready is bad or disrespectful. If the answer is you dont know, so be it, but I dont see how it hurts to ask.

@meejah
Copy link
Contributor

meejah commented Jun 16, 2017

Nothing disrespectful about asking!

The answer is: unknown. What @oberstet is getting at is that having a paying customer who wants this would speed it up dramatically :)

The reason I've been doing "Twisted only" side first is to make sure the API is right so that if it's not, we only have to change one framework's code. Also I know Twisted better than asyncio.

Of course, if you're interesting in helping by making a autobahn/asyncio/component.py that mirrors what's in autobahn/twisted/component.py currently, that would be very welcome! Ping us in #autobahn on Freenode if you need some guidance.

All that said, this API has decent consensus I think so I don't expect any major changes -- more like "possibly some tweaks".

@oberstet
Copy link
Contributor

Yeah, IMO the component API is definitely the way forward. The ApplicationRunner was basically a quick hack to collect the low-level boilerplate on top of ApplicationSession and the raw transport factory classes.

On a sidenote, the new AutobahnJava is following some crucial design improvements that the component API has, eg the session lifecycle can be tapped into from user code using the observer pattern.

@ashaffer
Copy link

ashaffer commented Jun 20, 2017

Hey, i'm having the exact same problem. Adding 'tls': True to the transport config just produces this error:

ValueError: 'tls' is not a valid configuration item

EDIT: I think I figured it out. For those wondering, the correct code is:

component = Component(
    realm=u'realm1',
    transports=[{
        'endpoint': {
            'type': 'tcp',
            'host': u'api.poloniex.com',
            'port': 443,            
            'tls': True
        },
        'type': 'websocket',
        'url': u'wss://api.poloniex.com',
        'options': {
            'open_handshake_timeout': 60.0
        }
    }]
)

@796F
Copy link

796F commented Jun 20, 2017

@ashaffer

I just tried that with a fresh install, getting this error:

2017-06-20T19:37:17+1000 connecting once using transport type "websocket" over endpoint "tcp"
2017-06-20T19:37:17+1000 component failed: TypeError: optionsForClientTLS requires text for host names, not str
2017-06-20T19:37:17+1000 Connection failed: TypeError: optionsForClientTLS requires text for host names, not str
2017-06-20T19:37:19+1000 connecting once using transport type "websocket" over endpoint "tcp"
2017-06-20T19:37:19+1000 component failed: TypeError: optionsForClientTLS requires text for host names, not str
2017-06-20T19:37:19+1000 Connection failed: TypeError: optionsForClientTLS requires text for host names, not str

any chance you ran into the same thing?

Appreciate the help!

Update:

dug into the source a bit and found this:

autobahn.wamp.component.py line 381

:param transports: Transport configurations for creating
            transports. Each transport can be a WAMP URL, or a dict
            containing the following configuration keys:

                - ``type`` (optional): ``websocket`` (default) or ``rawsocket``
                - ``url``: the WAMP URL
                - ``endpoint`` (optional, derived from URL if not provided):
                    - ``type``: "tcp" or "unix"
                    - ``host``, ``port``: only for TCP
                    - ``path``: only for unix
                    - ``timeout``: in seconds
                    - ``tls``: ``True`` or (under Twisted) an
                      ``twisted.internet.ssl.IOpenSSLClientComponentCreator``
                      instance (such as returned from
                      ``twisted.internet.ssl.optionsForClientTLS``) or
                      ``CertificateOptions`` instance.

So I changed

{
  "tls": True
}

to

from twisted.internet.ssl import CertificateOptions
{
  "tls": CertificateOptions()
}

and am now getting ticker prices.

2017-06-20T19:46:15+1000 BTC_ETH: 0.13982026 A:0.13997899 B:0.13982026 -0.00766619% V:27551.50744522 H:0.14120000 L:0.13720100
2017-06-20T19:46:15+1000 USDT_ZEC: 405.00000000 A:405.99999999 B:405.00000000 0.09446599% V:3132057.98862564 H:432.99999995 L:365.00000000

@ashaffer
Copy link

Nice! Ya, using CertificateOptions() is probably more right. On an unrelated note, do you happen to know if there's a recommended way of running one of these things in another thread? What i'd like to do is have a separate thread running my websocket connection, and then be able to subscribe/unsubscribe to different channels from the main thread, and have the websocket thread push messages over as they come in. It's not super obvious to me how to do that, though.

@meejah
Copy link
Contributor

meejah commented Jun 20, 2017

'tls': True should work fine, though so there's a problem here somewhere.. is this with Python2 or Python3?

You almost certainly do not want threads for what you're asking for. Are you doing some very CPU-intensive things somewhere?

@ashaffer
Copy link

ashaffer commented Jun 20, 2017

Nope, no CPU-intensive work. What I want is to be able to have multiple websocket clients running simultaneously, connected to different servers. And then I want code running 'above' them in the logical sense to be able to manage them (i.e. subscribe/unsubscribe to different channels, add event listeners for messages, etc..). You're certainly probably right that i'm doing it wrong, I haven't really messed with concurrency or asynchronous I/O in Python before.

@subiol
Copy link

subiol commented Jun 20, 2017

@ashaffer there is no point on using threads since the python GIL does not allow for two threads to run at the same time and the library already uses asyncio/twisted to deal with blocking calls. So using threads will (most likely) slightly decrease performance.

But if you still wanted to do it just because, you just need to pass messages between the threads, for example using a asyncio thread queue like janus https://github.com/aio-libs/janus .

@meejah
Copy link
Contributor

meejah commented Jun 20, 2017

@ashaffer you don't need threads for that when using Twisted (or asyncio)

@ashaffer
Copy link

@meejah Ya, I kind of suspected that. It didn't feel right, but it's not clear to me how else to do it, since the Component mechanism is all sort of encapsulated in itself, it can't easily be called from the outside. Would you mind pointing me towards some docs or in the right direction there?

@subiol
Copy link

subiol commented Jun 20, 2017

@ashaffer you have two options:

  1. make all your code asynchronous, so you use asyncio or twisted style coding in one thread together with the websocket.

  2. Put all the websocket code in its own thread and communicate with your threads using a push/pull queue like the already mentioned Janus (and I'm sure you can find something equivalent for twisted). In the Janus readme you can find a code example on how to communicate an asyncio thread (in your case the websocket thread) with other threads (in your case the rest of your program).

Note that in case 2 your program will still use only one core since the python GIL does not allow two threads to run at the same time. The difference is that in case 1 you decide in which order your code executes, while in case 2 the python thread scheduler will switch between the threads as its see fit, so it should be slightly less efficient.

This should solve your doubts, since I think we have already dirtied the issue with enough of topic.

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

Successfully merging a pull request may close this issue.

9 participants