1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976
|
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Crossbar.io Technologies 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
|