123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976 |
- ###############################################################################
- #
- # The MIT License (MIT)
- #
- # Copyright (c) typedef int GmbH
- #
- # Permission is hereby granted, free of charge, to any person obtaining a copy
- # of this software and associated documentation files (the "Software"), to deal
- # in the Software without restriction, including without limitation the rights
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- # copies of the Software, and to permit persons to whom the Software is
- # furnished to do so, subject to the following conditions:
- #
- # The above copyright notice and this permission notice shall be included in
- # all copies or substantial portions of the Software.
- #
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- # THE SOFTWARE.
- #
- ###############################################################################
-
-
- import itertools
- import random
- from functools import partial
-
- import txaio
-
- from autobahn.util import ObservableMixin
- from autobahn.websocket.util import parse_url as parse_ws_url
- from autobahn.rawsocket.util import parse_url as parse_rs_url
-
- from autobahn.wamp.types import ComponentConfig, SubscribeOptions, RegisterOptions
- from autobahn.wamp.exception import SessionNotReady, ApplicationError
- from autobahn.wamp.auth import create_authenticator, IAuthenticator
- from autobahn.wamp.serializer import SERID_TO_SER
-
-
- __all__ = (
- 'Component'
- )
-
-
- def _validate_endpoint(endpoint, check_native_endpoint=None):
- """
- Check a WAMP connecting endpoint configuration.
- """
- if check_native_endpoint:
- check_native_endpoint(endpoint)
- elif not isinstance(endpoint, dict):
- raise ValueError(
- "'endpoint' must be a dict"
- )
-
- # note, we're falling through here -- check_native_endpoint can
- # disallow or allow dict-based config as it likes, but if it
- # *does* allow a dict through, we want to check "base options"
- # here so that both Twisted and asyncio don't have to check these
- # things as well.
- if isinstance(endpoint, dict):
- # XXX what about filling in anything missing from the URL? Or
- # is that only for when *nothing* is provided for endpoint?
- if 'type' not in endpoint:
- # could maybe just make tcp the default?
- raise ValueError("'type' required in endpoint configuration")
- if endpoint['type'] not in ['tcp', 'unix']:
- raise ValueError('invalid type "{}" in endpoint'.format(endpoint['type']))
-
- for k in endpoint.keys():
- if k not in ['type', 'host', 'port', 'path', 'tls', 'timeout', 'version']:
- raise ValueError(
- "Invalid key '{}' in endpoint configuration".format(k)
- )
-
- if endpoint['type'] == 'tcp':
- for k in ['host', 'port']:
- if k not in endpoint:
- raise ValueError(
- "'{}' required in 'tcp' endpoint config".format(k)
- )
- for k in ['path']:
- if k in endpoint:
- raise ValueError(
- "'{}' not valid in 'tcp' endpoint config".format(k)
- )
- elif endpoint['type'] == 'unix':
- for k in ['path']:
- if k not in endpoint:
- raise ValueError(
- "'{}' required for 'unix' endpoint config".format(k)
- )
- for k in ['host', 'port', 'tls']:
- if k in endpoint:
- raise ValueError(
- "'{}' not valid in 'unix' endpoint config".format(k)
- )
- else:
- assert False, 'should not arrive here'
-
-
- def _create_transport(index, transport, check_native_endpoint=None):
- """
- Internal helper to insert defaults and create _Transport instances.
-
- :param transport: a (possibly valid) transport configuration
- :type transport: dict
-
- :returns: a _Transport instance
-
- :raises: ValueError on invalid configuration
- """
- if type(transport) != dict:
- raise ValueError('invalid type {} for transport configuration - must be a dict'.format(type(transport)))
-
- valid_transport_keys = [
- 'type', 'url', 'endpoint', 'serializer', 'serializers', 'options',
- 'max_retries', 'max_retry_delay', 'initial_retry_delay',
- 'retry_delay_growth', 'retry_delay_jitter', 'proxy',
- ]
- for k in transport.keys():
- if k not in valid_transport_keys:
- raise ValueError(
- "'{}' is not a valid configuration item".format(k)
- )
-
- kind = 'websocket'
- if 'type' in transport:
- if transport['type'] not in ['websocket', 'rawsocket']:
- raise ValueError('Invalid transport type {}'.format(transport['type']))
- kind = transport['type']
- else:
- transport['type'] = 'websocket'
-
- if 'proxy' in transport and kind != 'websocket':
- raise ValueError(
- "proxy= only supported for type=websocket transports"
- )
- proxy = transport.get("proxy", None)
- if proxy is not None:
- for k in proxy.keys():
- if k not in ['host', 'port']:
- raise ValueError(
- "Unknown key '{}' in proxy config".format(k)
- )
- for k in ['host', 'port']:
- if k not in proxy:
- raise ValueError(
- "Proxy config requires '{}'".formaT(k)
- )
-
- options = dict()
- if 'options' in transport:
- options = transport['options']
- if not isinstance(options, dict):
- raise ValueError(
- 'options must be a dict, not {}'.format(type(options))
- )
-
- if kind == 'websocket':
- for key in ['url']:
- if key not in transport:
- raise ValueError("Transport requires '{}' key".format(key))
- # endpoint not required; we will deduce from URL if it's not provided
- # XXX not in the branch I rebased; can this go away? (is it redundant??)
- if 'endpoint' not in transport:
- is_secure, host, port, resource, path, params = parse_ws_url(transport['url'])
- endpoint_config = {
- 'type': 'tcp',
- 'host': host,
- 'port': port,
- 'tls': is_secure,
- }
- else:
- # note: we're avoiding mutating the incoming "configuration"
- # dict, so this should avoid that too...
- endpoint_config = transport['endpoint']
- _validate_endpoint(endpoint_config, check_native_endpoint)
-
- if 'serializer' in transport:
- raise ValueError("'serializer' is only for rawsocket; use 'serializers'")
- if 'serializers' in transport:
- if not isinstance(transport['serializers'], (list, tuple)):
- raise ValueError("'serializers' must be a list of strings")
- if not all([
- isinstance(s, (str, str))
- for s in transport['serializers']]):
- raise ValueError("'serializers' must be a list of strings")
- valid_serializers = SERID_TO_SER.keys()
- for serial in transport['serializers']:
- if serial not in valid_serializers:
- raise ValueError(
- "Invalid serializer '{}' (expected one of: {})".format(
- serial,
- ', '.join([repr(s) for s in valid_serializers]),
- )
- )
- serializer_config = transport.get('serializers', ['cbor', 'json'])
-
- elif kind == 'rawsocket':
- if 'endpoint' not in transport:
- if transport['url'].startswith('rs'):
- # # try to parse RawSocket URL ..
- isSecure, host, port = parse_rs_url(transport['url'])
- elif transport['url'].startswith('ws'):
- # try to parse WebSocket URL ..
- isSecure, host, port, resource, path, params = parse_ws_url(transport['url'])
- else:
- raise RuntimeError()
- if host == 'unix':
- # here, "port" is actually holding the path on the host, eg "/tmp/file.sock"
- endpoint_config = {
- 'type': 'unix',
- 'path': port,
- }
- else:
- endpoint_config = {
- 'type': 'tcp',
- 'host': host,
- 'port': port,
- }
- else:
- endpoint_config = transport['endpoint']
- if 'serializers' in transport:
- raise ValueError("'serializers' is only for websocket; use 'serializer'")
- # always a list; len == 1 for rawsocket
- if 'serializer' in transport:
- if not isinstance(transport['serializer'], (str, str)):
- raise ValueError("'serializer' must be a string")
- serializer_config = [transport['serializer']]
- else:
- serializer_config = ['cbor']
-
- else:
- assert False, 'should not arrive here'
-
- kw = {}
- for key in ['max_retries', 'max_retry_delay', 'initial_retry_delay',
- 'retry_delay_growth', 'retry_delay_jitter']:
- if key in transport:
- kw[key] = transport[key]
-
- return _Transport(
- index,
- kind=kind,
- url=transport.get('url', None),
- endpoint=endpoint_config,
- serializers=serializer_config,
- proxy=proxy,
- options=options,
- **kw
- )
-
-
- class _Transport(object):
- """
- Thin-wrapper for WAMP transports used by a Connection.
- """
-
- def __init__(self, idx, kind, url, endpoint, serializers,
- max_retries=-1,
- max_retry_delay=300,
- initial_retry_delay=1.5,
- retry_delay_growth=1.5,
- retry_delay_jitter=0.1,
- proxy=None,
- options=None):
- """
- """
- if options is None:
- options = dict()
- self.idx = idx
-
- self.type = kind
- self.url = url
- self.endpoint = endpoint
- self.options = options
-
- self.serializers = serializers
- if self.type == 'rawsocket' and len(serializers) != 1:
- raise ValueError(
- "'rawsocket' transport requires exactly one serializer"
- )
-
- self.max_retries = max_retries
- self.max_retry_delay = max_retry_delay
- self.initial_retry_delay = initial_retry_delay
- self.retry_delay_growth = retry_delay_growth
- self.retry_delay_jitter = retry_delay_jitter
- self.proxy = proxy # this is a dict of proxy config
-
- # used via can_reconnect() and failed() to record this
- # transport is never going to work
- self._permanent_failure = False
-
- self.reset()
-
- def reset(self):
- """
- set connection failure rates and retry-delay to initial values
- """
- self.connect_attempts = 0
- self.connect_sucesses = 0
- self.connect_failures = 0
- self.retry_delay = self.initial_retry_delay
-
- def failed(self):
- """
- Mark this transport as failed, meaning we won't try to connect to
- it any longer (that is: can_reconnect() will always return
- False afer calling this).
- """
- self._permanent_failure = True
-
- def can_reconnect(self):
- if self._permanent_failure:
- return False
- if self.max_retries == -1:
- return True
- return self.connect_attempts < self.max_retries + 1
-
- def next_delay(self):
- if self.connect_attempts == 0:
- # if we never tried before, try immediately
- return 0
- elif self.max_retries != -1 and self.connect_attempts >= self.max_retries + 1:
- raise RuntimeError('max reconnects reached')
- else:
- self.retry_delay = self.retry_delay * self.retry_delay_growth
- self.retry_delay = random.normalvariate(self.retry_delay, self.retry_delay * self.retry_delay_jitter)
- if self.retry_delay > self.max_retry_delay:
- self.retry_delay = self.max_retry_delay
- return self.retry_delay
-
- def describe_endpoint(self):
- """
- returns a human-readable description of the endpoint
- """
- if isinstance(self.endpoint, dict):
- return self.endpoint['type']
- return repr(self.endpoint)
-
-
- # this could probably implement twisted.application.service.IService
- # if we wanted; or via an adapter...which just adds a startService()
- # and stopService() [latter can be async]
-
- class Component(ObservableMixin):
- """
- A WAMP application component. A component holds configuration for
- (and knows how to create) transports and sessions.
- """
-
- session_factory = None
- """
- The factory of the session we will instantiate.
- """
-
- def subscribe(self, topic, options=None, check_types=False):
- """
- A decorator as a shortcut for subscribing during on-join
-
- For example::
-
- @component.subscribe(
- "some.topic",
- options=SubscribeOptions(match='prefix'),
- )
- def topic(*args, **kw):
- print("some.topic({}, {}): event received".format(args, kw))
- """
- assert options is None or isinstance(options, SubscribeOptions)
-
- def decorator(fn):
-
- def do_subscription(session, details):
- return session.subscribe(fn, topic=topic, options=options, check_types=check_types)
- self.on('join', do_subscription)
- return fn
- return decorator
-
- def register(self, uri, options=None, check_types=False):
- """
- A decorator as a shortcut for registering during on-join
-
- For example::
-
- @component.register(
- "com.example.add",
- options=RegisterOptions(invoke='roundrobin'),
- )
- def add(*args, **kw):
- print("add({}, {}): event received".format(args, kw))
- """
- assert options is None or isinstance(options, RegisterOptions)
-
- def decorator(fn):
-
- def do_registration(session, details):
- return session.register(fn, procedure=uri, options=options, check_types=check_types)
- self.on('join', do_registration)
- return fn
- return decorator
-
- def __init__(self, main=None, transports=None, config=None, realm='realm1', extra=None,
- authentication=None, session_factory=None, is_fatal=None):
- """
- :param main: After a transport has been connected and a session
- has been established and joined to a realm, this (async)
- procedure will be run until it finishes -- which signals that
- the component has run to completion. In this case, it usually
- doesn't make sense to use the ``on_*`` kwargs. If you do not
- pass a main() procedure, the session will not be closed
- (unless you arrange for .leave() to be called).
-
- :type main: callable taking two args ``reactor`` and ``ISession``
-
- :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 router 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.
- - ``max_retries``: Maximum number of reconnection attempts. Unlimited if set to -1.
- - ``initial_retry_delay``: Initial delay for reconnection attempt in seconds (Default: 1.0s).
- - ``max_retry_delay``: Maximum delay for reconnection attempts in seconds (Default: 60s).
- - ``retry_delay_growth``: The growth factor applied to the retry delay between reconnection attempts (Default 1.5).
- - ``retry_delay_jitter``: A 0-argument callable that introduces nose into the delay. (Default random.random)
- - ``serializer`` (only for raw socket): Specify an accepted serializer (e.g. 'json', 'msgpack', 'cbor', 'ubjson', 'flatbuffers')
- - ``serializers``: Specify list of accepted serializers
- - ``options``: tbd
- - ``proxy``: tbd
-
- :type transports: None or str or list
-
- :param realm: the realm to join
- :type realm: str
-
- :param authentication: configuration of authenticators
- :type authentication: dict
-
- :param session_factory: if None, ``ApplicationSession`` is
- used, otherwise a callable taking a single ``config`` argument
- that is used to create a new `ApplicationSession` instance.
-
- :param is_fatal: a callable taking a single argument, an
- ``Exception`` instance. The callable should return ``True`` if
- this error is "fatal", meaning we should not try connecting to
- the current transport again. The default behavior (on None) is
- to always return ``False``
- """
- self.set_valid_events(
- [
- 'start', # fired by base class
- 'connect', # fired by ApplicationSession
- 'join', # fired by ApplicationSession
- 'ready', # fired by ApplicationSession
- 'leave', # fired by ApplicationSession
- 'disconnect', # fired by ApplicationSession
- 'connectfailure', # fired by base class
- ]
- )
-
- if is_fatal is not None and not callable(is_fatal):
- raise ValueError('"is_fatal" must be a callable or None')
- self._is_fatal = is_fatal
-
- if main is not None and not callable(main):
- raise ValueError('"main" must be a callable if given')
- self._entry = main
-
- # use WAMP-over-WebSocket to localhost when no transport is specified at all
- if transports is None:
- transports = 'ws://127.0.0.1:8080/ws'
-
- # allows to provide a URL instead of a list of transports
- if isinstance(transports, (str, str)):
- url = transports
- # 'endpoint' will get filled in by parsing the 'url'
- transport = {
- 'type': 'websocket',
- 'url': url,
- }
- transports = [transport]
-
- # allows single transport instead of a list (convenience)
- elif isinstance(transports, dict):
- transports = [transports]
-
- # XXX do we want to be able to provide an infinite iterable of
- # transports here? e.g. a generator that makes new transport
- # to try?
-
- # now check and save list of transports
- self._transports = []
- for idx, transport in enumerate(transports):
- # allows to provide a URL instead of transport dict
- if type(transport) == str:
- _transport = {
- 'type': 'websocket',
- 'url': transport,
- }
- else:
- _transport = transport
- self._transports.append(
- _create_transport(idx, _transport, self._check_native_endpoint)
- )
-
- # XXX should have some checkconfig support
- self._authentication = authentication or {}
-
- if session_factory:
- self.session_factory = session_factory
- self._realm = realm
- self._extra = extra
-
- self._delay_f = None
- self._done_f = None
- self._session = None
- self._stopping = False
-
- def _can_reconnect(self):
- # check if any of our transport has any reconnect attempt left
- for transport in self._transports:
- if transport.can_reconnect():
- return True
- return False
-
- def _start(self, loop=None):
- """
- This starts the Component, which means it will start connecting
- (and re-connecting) to its configured transports. A Component
- runs until it is "done", which means one of:
-
- - There was a "main" function defined, and it completed successfully;
- - Something called ``.leave()`` on our session, and we left successfully;
- - ``.stop()`` was called, and completed successfully;
- - none of our transports were able to connect successfully (failure);
-
- :returns: a Future/Deferred which will resolve (to ``None``) when we are
- "done" or with an error if something went wrong.
- """
-
- # we can only be "start()ed" once before we stop .. but that
- # doesn't have to be an error we can give back another future
- # that fires when our "real" _done_f is completed.
- if self._done_f is not None:
- d = txaio.create_future()
-
- def _cb(arg):
- txaio.resolve(d, arg)
-
- txaio.add_callbacks(self._done_f, _cb, _cb)
- return d
-
- # this future will be returned, and thus has the semantics
- # specified in the docstring.
- self._done_f = txaio.create_future()
-
- def _reset(arg):
- """
- if the _done_f future is resolved (good or bad), we want to set it
- to None in our class
- """
- self._done_f = None
- return arg
- txaio.add_callbacks(self._done_f, _reset, _reset)
-
- # Create a generator of transports that .can_reconnect()
- transport_gen = itertools.cycle(self._transports)
-
- # this is a 1-element list so we can set it from closures in
- # this function
- transport_candidate = [0]
-
- def error(fail):
- self._delay_f = None
- if self._stopping:
- # might be better to add framework-specific checks in
- # subclasses to see if this is CancelledError (for
- # Twisted) and whatever asyncio does .. but tracking
- # if we're in the shutdown path is fine too
- txaio.resolve(self._done_f, None)
- else:
- self.log.info("Internal error {msg}", msg=txaio.failure_message(fail))
- self.log.debug("{tb}", tb=txaio.failure_format_traceback(fail))
- txaio.reject(self._done_f, fail)
-
- def attempt_connect(_):
- self._delay_f = None
-
- def handle_connect_error(fail):
- # FIXME - make txaio friendly
- # Can connect_f ever be in a cancelled state?
- # if txaio.using_asyncio and isinstance(fail.value, asyncio.CancelledError):
- # unrecoverable_error = True
-
- self.log.debug('component failed: {error}', error=txaio.failure_message(fail))
- self.log.debug('{tb}', tb=txaio.failure_format_traceback(fail))
- # If this is a "fatal error" that will never work,
- # we bail out now
- if isinstance(fail.value, ApplicationError):
- self.log.error("{msg}", msg=fail.value.error_message())
-
- elif isinstance(fail.value, OSError):
- # failed to connect entirely, like nobody
- # listening etc.
- self.log.info("Connection failed with OS error: {msg}", msg=txaio.failure_message(fail))
-
- elif self._is_ssl_error(fail.value):
- # Quoting pyOpenSSL docs: "Whenever
- # [SSL.Error] is raised directly, it has a
- # list of error messages from the OpenSSL
- # error queue, where each item is a tuple
- # (lib, function, reason). Here lib, function
- # and reason are all strings, describing where
- # and what the problem is. See err(3) for more
- # information."
- # (and 'args' is a 1-tuple containing the above
- # 3-tuple...)
- ssl_lib, ssl_func, ssl_reason = fail.value.args[0][0]
- self.log.error("TLS failure: {reason}", reason=ssl_reason)
- else:
- self.log.error(
- 'Connection failed: {error}',
- error=txaio.failure_message(fail),
- )
-
- if self._is_fatal is None:
- is_fatal = False
- else:
- is_fatal = self._is_fatal(fail.value)
- if is_fatal:
- self.log.info("Error was fatal; failing transport")
- transport_candidate[0].failed()
-
- txaio.call_later(0, transport_check, None)
- return
-
- def notify_connect_error(fail):
- chain_f = txaio.create_future()
- # hmm, if connectfailure took a _Transport instead of
- # (or in addition to?) self it could .failed() the
- # transport and we could do away with the is_fatal
- # listener?
- handler_f = self.fire('connectfailure', self, fail.value)
- txaio.add_callbacks(
- handler_f,
- lambda _: txaio.reject(chain_f, fail),
- lambda _: txaio.reject(chain_f, fail)
- )
- return chain_f
-
- def connect_error(fail):
- notify_f = notify_connect_error(fail)
- txaio.add_callbacks(notify_f, None, handle_connect_error)
-
- def session_done(x):
- txaio.resolve(self._done_f, None)
-
- connect_f = txaio.as_future(
- self._connect_once,
- loop, transport_candidate[0],
- )
- txaio.add_callbacks(connect_f, session_done, connect_error)
-
- def transport_check(_):
- self.log.debug('Entering re-connect loop')
-
- if not self._can_reconnect():
- err_msg = "Component failed: Exhausted all transport connect attempts"
- self.log.info(err_msg)
- try:
- raise RuntimeError(err_msg)
- except RuntimeError as e:
- txaio.reject(self._done_f, e)
- return
-
- while True:
-
- transport = next(transport_gen)
-
- if transport.can_reconnect():
- transport_candidate[0] = transport
- break
-
- delay = transport.next_delay()
- self.log.warn(
- 'trying transport {transport_idx} ("{transport_url}") using connect delay {transport_delay}',
- transport_idx=transport.idx,
- transport_url=transport.url,
- transport_delay=delay,
- )
-
- self._delay_f = txaio.sleep(delay)
- txaio.add_callbacks(self._delay_f, attempt_connect, error)
-
- # issue our first event, then start reconnect loop
- start_f = self.fire('start', loop, self)
- txaio.add_callbacks(start_f, transport_check, error)
- return self._done_f
-
- def stop(self):
- self._stopping = True
- if self._session and self._session.is_attached():
- return self._session.leave()
- elif self._delay_f:
- # This cancel request will actually call the "error" callback of
- # the _delay_f future. Nothing to worry about.
- return txaio.as_future(txaio.cancel, self._delay_f)
- # if (for some reason -- should we log warning here to figure
- # out if this can evern happen?) we've not fired _done_f, we
- # do that now (causing our "main" to exit, and thus react() to
- # quit)
- if not txaio.is_called(self._done_f):
- txaio.resolve(self._done_f, None)
- return txaio.create_future_success(None)
-
- def _connect_once(self, reactor, transport):
-
- self.log.info(
- 'connecting once using transport type "{transport_type}" '
- 'over endpoint "{endpoint_desc}"',
- transport_type=transport.type,
- endpoint_desc=transport.describe_endpoint(),
- )
-
- done = txaio.create_future()
-
- # factory for ISession objects
- def create_session():
- cfg = ComponentConfig(self._realm, self._extra)
- try:
- self._session = session = self.session_factory(cfg)
- for auth_name, auth_config in self._authentication.items():
- if isinstance(auth_config, IAuthenticator):
- session.add_authenticator(auth_config)
- else:
- authenticator = create_authenticator(auth_name, **auth_config)
- session.add_authenticator(authenticator)
-
- except Exception as e:
- # couldn't instantiate session calls, which is fatal.
- # let the reconnection logic deal with that
- f = txaio.create_failure(e)
- txaio.reject(done, f)
- raise
- else:
- # hook up the listener to the parent so we can bubble
- # up events happning on the session onto the
- # connection. This lets you do component.on('join',
- # cb) which will work just as if you called
- # session.on('join', cb) for every session created.
- session._parent = self
-
- # listen on leave events; if we get errors
- # (e.g. no_such_realm), an on_leave can happen without
- # an on_join before
- def on_leave(session, details):
- self.log.info(
- "session leaving '{details.reason}'",
- details=details,
- )
- if not txaio.is_called(done):
- if details.reason in ["wamp.close.normal", "wamp.close.goodbye_and_out"]:
- txaio.resolve(done, None)
- else:
- f = txaio.create_failure(
- ApplicationError(details.reason, details.message)
- )
- txaio.reject(done, f)
- session.on('leave', on_leave)
-
- # if we were given a "main" procedure, we run through
- # it completely (i.e. until its Deferred fires) and
- # then disconnect this session
- def on_join(session, details):
- transport.reset()
- transport.connect_sucesses += 1
- self.log.debug("session on_join: {details}", details=details)
- d = txaio.as_future(self._entry, reactor, session)
-
- def main_success(_):
- self.log.debug("main_success")
-
- def leave():
- try:
- session.leave()
- except SessionNotReady:
- # someone may have already called
- # leave()
- pass
- txaio.call_later(0, leave)
-
- def main_error(err):
- self.log.debug("main_error: {err}", err=err)
- txaio.reject(done, err)
- session.disconnect()
- txaio.add_callbacks(d, main_success, main_error)
- if self._entry is not None:
- session.on('join', on_join)
-
- # listen on disconnect events. Note that in case we
- # had a "main" procedure, we could have already
- # resolve()'d our "done" future
- def on_disconnect(session, was_clean):
- self.log.debug(
- "session on_disconnect: was_clean={was_clean}",
- was_clean=was_clean,
- )
- if not txaio.is_called(done):
- if not was_clean:
- self.log.warn(
- "Session disconnected uncleanly"
- )
- else:
- # eg the session has left the realm, and the transport was properly
- # shut down. successfully finish the connection
- txaio.resolve(done, None)
- session.on('disconnect', on_disconnect)
-
- # return the fresh session object
- return session
-
- transport.connect_attempts += 1
-
- d = txaio.as_future(
- self._connect_transport,
- reactor, transport, create_session, done,
- )
-
- def on_error(err):
- """
- this may seem redundant after looking at _connect_transport, but
- it will handle a case where something goes wrong in
- _connect_transport itself -- as the only connect our
- caller has is the 'done' future
- """
- transport.connect_failures += 1
- # something bad has happened, and maybe didn't get caught
- # upstream yet
- if not txaio.is_called(done):
- txaio.reject(done, err)
- txaio.add_callbacks(d, None, on_error)
-
- return done
-
- def on_join(self, fn):
- """
- A decorator as a shortcut for listening for 'join' events.
-
- For example::
-
- @component.on_join
- def joined(session, details):
- print("Session {} joined: {}".format(session, details))
- """
- self.on('join', fn)
-
- def on_leave(self, fn):
- """
- A decorator as a shortcut for listening for 'leave' events.
- """
- self.on('leave', fn)
-
- def on_connect(self, fn):
- """
- A decorator as a shortcut for listening for 'connect' events.
- """
- self.on('connect', fn)
-
- def on_disconnect(self, fn):
- """
- A decorator as a shortcut for listening for 'disconnect' events.
- """
- self.on('disconnect', fn)
-
- def on_ready(self, fn):
- """
- A decorator as a shortcut for listening for 'ready' events.
- """
- self.on('ready', fn)
-
- def on_connectfailure(self, fn):
- """
- A decorator as a shortcut for listening for 'connectfailure' events.
- """
- self.on('connectfailure', fn)
-
-
- def _run(reactor, components, done_callback=None):
- """
- Internal helper. Use "run" method from autobahn.twisted.wamp or
- autobahn.asyncio.wamp
-
- This is the generic parts of the run() method so that there's very
- little code in the twisted/asyncio specific run() methods.
-
- This is called by react() (or run_until_complete() so any errors
- coming out of this should be handled properly. Logging will
- already be started.
- """
- # let user pass a single component to run, too
- # XXX probably want IComponent? only demand it, here and below?
- if isinstance(components, Component):
- components = [components]
-
- if type(components) != list:
- raise ValueError(
- '"components" must be a list of Component objects - encountered'
- ' {0}'.format(type(components))
- )
-
- for c in components:
- if not isinstance(c, Component):
- raise ValueError(
- '"components" must be a list of Component objects - encountered'
- 'item of type {0}'.format(type(c))
- )
-
- # validation complete; proceed with startup
- log = txaio.make_logger()
-
- def component_success(comp, arg):
- log.debug("Component '{c}' successfully completed: {arg}", c=comp, arg=arg)
- return arg
-
- def component_failure(comp, f):
- log.error("Component '{c}' error: {msg}", c=comp, msg=txaio.failure_message(f))
- log.debug("Component error: {tb}", tb=txaio.failure_format_traceback(f))
- # double-check: is a component-failure still fatal to the
- # startup process (because we passed consume_exception=False
- # to gather() below?)
- return None
-
- def component_start(comp):
- # the future from start() errbacks if we fail, or callbacks
- # when the component is considered "done" (so maybe never)
- d = txaio.as_future(comp.start, reactor)
- txaio.add_callbacks(
- d,
- partial(component_success, comp),
- partial(component_failure, comp),
- )
- return d
-
- # note that these are started in parallel -- maybe we want to add
- # a "connected" signal to components so we could start them in the
- # order they're given to run() as "a" solution to dependencies.
- dl = []
- for comp in components:
- d = component_start(comp)
- dl.append(d)
- done_d = txaio.gather(dl, consume_exceptions=False)
-
- if done_callback:
- def all_done(arg):
- log.debug("All components ended; stopping reactor")
- done_callback(reactor, arg)
- txaio.add_callbacks(done_d, all_done, all_done)
-
- return done_d
|