コード例 #1
0
class NetworkConnector:

    INCOMING_WS_PATH = "ws.incoming"
    OUTGOING_WS_PATH = "ws.outgoing"
    SYSTEM_ALERT = "system.alerts"
    SERVER_CONFIG = "system.config"
    MAX_PAYLOAD = "max.payload"
    DISTRIBUTED_TRACING = "distributed.tracing"

    def __init__(self, platform, distributed_trace, loop, url_list, origin):
        self.platform = platform
        self._distributed_trace = distributed_trace
        self._loop = loop
        self.log = platform.log
        self.normal = True
        self.started = False
        self.ready = False
        self.ws = None
        self.close_code = 1000
        self.close_message = 'OK'
        self.last_active = time.time()
        self.max_ws_payload = 32768
        self.util = Utility()
        self.urls = self.util.multi_split(url_list, ', ')
        self.next_url = 1
        self.origin = origin
        self.cache = SimpleCache(loop, self.log, timeout_seconds=30)
        self.api_key = self._get_api_key()

    def _get_api_key(self):
        config = AppConfig()
        if config.API_KEY_LOCATION in os.environ:
            self.log.info('Found API key in environment variable ' +
                          config.API_KEY_LOCATION)
            return os.environ[config.API_KEY_LOCATION]
        # check temp file system because API key not in environment
        temp_dir = '/tmp/config'
        if not os.path.exists(temp_dir):
            os.makedirs(temp_dir)
        api_key_file = temp_dir + "/lang-api-key.txt"
        if os.path.exists(api_key_file):
            with open(api_key_file) as f:
                self.log.info('Reading API key from ' + api_key_file)
                return f.read().strip()
        else:
            with open(api_key_file, 'w') as f:
                self.log.info(
                    'Generating new API key in ' + api_key_file +
                    ' because it is not found in environment variable ' +
                    config.API_KEY_LOCATION)
                value = ''.join(str(uuid.uuid4()).split('-'))
                f.write(value + '\n')
                return value

    def _get_next_url(self):
        # index starts from 1
        return self.urls[self.next_url - 1]

    def _skip_url(self):
        self.next_url += 1
        if self.next_url > len(self.urls):
            self.next_url = 1

    def send_keep_alive(self):
        message = "Keep-Alive " + self.util.get_iso_8601(time.time(),
                                                         show_ms=True)
        envelope = EventEnvelope()
        envelope.set_to(self.OUTGOING_WS_PATH).set_header(
            'type', 'text').set_body(message)
        self.platform.send_event(envelope)

    def send_payload(self, data: dict):
        payload = msgpack.packb(data, use_bin_type=True)
        payload_len = len(payload)
        if 'type' in data and data[
                'type'] == 'event' and 'event' in data and payload_len > self.max_ws_payload:
            evt = data['event']
            if 'id' in evt:
                msg_id = evt['id']
                total = int(payload_len / self.max_ws_payload)
                if payload_len > total:
                    total += 1
                buffer = io.BytesIO(payload)
                count = 0
                for i in range(total):
                    count += 1
                    block = EventEnvelope()
                    block.set_header('id', msg_id)
                    block.set_header('count', count)
                    block.set_header('total', total)
                    block.set_body(buffer.read(self.max_ws_payload))
                    block_map = dict()
                    block_map['type'] = 'block'
                    block_map['block'] = block.to_map()
                    block_payload = msgpack.packb(block_map, use_bin_type=True)
                    envelope = EventEnvelope()
                    envelope.set_to(self.OUTGOING_WS_PATH).set_header(
                        'type', 'bytes').set_body(block_payload)
                    self.platform.send_event(envelope)
        else:
            envelope = EventEnvelope()
            envelope.set_to(self.OUTGOING_WS_PATH).set_header(
                'type', 'bytes').set_body(payload)
            self.platform.send_event(envelope)

    def _get_server_config(self, headers: dict, body: any):
        if 'type' in headers:
            # at this point, login is successful
            if headers['type'] == 'system.config' and isinstance(body, dict):
                if self.MAX_PAYLOAD in body:
                    self.max_ws_payload = body[self.MAX_PAYLOAD]
                    self.log.info(
                        'Automatic segmentation when event payload exceeds ' +
                        format(self.max_ws_payload, ',d'))
                # advertise public routes to language connector
                for r in self.platform.get_routes('public'):
                    self.send_payload({'type': 'add', 'route': r})
                # tell server that I am ready
                self.send_payload({'type': 'ready'})
            # server acknowledges my ready signal
            if headers['type'] == 'ready':
                self.ready = True
                self.log.info('Ready')
                # redo subscription if any
                if self.platform.has_route('pub.sub.sync'):
                    event = EventEnvelope()
                    event.set_to('pub.sub.sync').set_header(
                        'type', 'subscription_sync')
                    self.platform.send_event(event)

    def _alert(self, headers: dict, body: any):
        if 'status' in headers:
            if headers['status'] == '200':
                self.log.info(str(body))
            else:
                self.log.warn(str(body) + ", status=" + headers['status'])

    def _incoming(self, headers: dict, body: any):
        """
        This function handles incoming messages from the websocket connection with the Mercury language connector.
        It must be invoked using events. It should not be called directly to guarantee proper event sequencing.

        :param headers: type is open, close, text or bytes
        :param body: string or bytes
        :return: None
        """
        if self.ws and 'type' in headers:
            if headers['type'] == 'open':
                self.ready = False
                self.log.info("Login to language connector")
                self.send_payload({'type': 'login', 'api_key': self.api_key})
            if headers['type'] == 'close':
                self.ready = False
                self.log.info("Closed")
            if headers['type'] == 'text':
                self.log.debug(body)
            if headers['type'] == 'bytes':
                event = msgpack.unpackb(body, raw=False)
                if 'type' in event:
                    event_type = event['type']
                    if event_type == 'block' and 'block' in event:
                        envelope = EventEnvelope()
                        inner_event = envelope.from_map(event['block'])
                        inner_headers = inner_event.get_headers()
                        if 'id' in inner_headers and 'count' in inner_headers and 'total' in inner_headers:
                            msg_id = inner_headers['id']
                            msg_count = inner_headers['count']
                            msg_total = inner_headers['total']
                            data = inner_event.get_body()
                            if isinstance(data, bytes):
                                buffer = self.cache.get(msg_id)
                                if buffer is None:
                                    buffer = io.BytesIO()
                                buffer.write(data)
                                self.cache.put(msg_id, buffer)
                                if msg_count == msg_total:
                                    buffer.seek(0)
                                    # reconstruct event for processing
                                    event = msgpack.unpackb(buffer.read(),
                                                            raw=False)
                                    event_type = 'event'
                                    self.cache.remove(msg_id)
                    if event_type == 'event' and 'event' in event:
                        envelope = EventEnvelope()
                        inner_event = envelope.from_map(event['event'])
                        if self.platform.has_route(inner_event.get_to()):
                            self.platform.send_event(inner_event)
                        else:
                            self.log.warn('Incoming event dropped because ' +
                                          str(inner_event.get_to()) +
                                          ' not found')

    def _outgoing(self, headers: dict, body: any):
        """
        This function handles sending outgoing messages to the websocket connection with the Mercury language connector.
        It must be invoked using events. It should not be called directly to guarantee proper event sequencing.

        :param headers: type is close, text or bytes
        :param body: string or bytes
        :return: None
        """
        if 'type' in headers:
            if headers['type'] == 'close':
                code = 1000 if 'code' not in headers else headers['code']
                reason = 'OK' if 'reason' not in headers else headers['reason']
                self.close_connection(code, reason)
            if headers['type'] == 'text':
                self._send_text(body)
            if headers['type'] == 'bytes':
                self._send_bytes(body)

    def _send_text(self, body: str):
        def send(data: str):
            async def async_send(d: str):
                await self.ws.send_str(d)

            self._loop.create_task(async_send(data))

        if self.is_connected():
            self._loop.call_soon_threadsafe(send, body)

    def _send_bytes(self, body: bytes):
        def send(data: bytes):
            async def async_send(d: bytes):
                await self.ws.send_bytes(d)

            self._loop.create_task(async_send(data))

        if self.is_connected():
            self._loop.call_soon_threadsafe(send, body)

    def is_connected(self):
        return self.started and self.ws

    def is_ready(self):
        return self.is_connected() and self.ready

    def start_connection(self):
        async def worker():
            while self.normal:
                await self._loop.create_task(
                    self.connection_handler(self._get_next_url()))
                # check again because the handler may have run for a while
                if self.normal:
                    # retry connection in 5 seconds
                    for _ in range(10):
                        await asyncio.sleep(0.5)
                        if not self.normal:
                            break
                else:
                    break

        if not self.started:
            self.started = True
            self.platform.register(self.DISTRIBUTED_TRACING,
                                   self._distributed_trace.logger,
                                   1,
                                   is_private=True)
            self.platform.register(self.INCOMING_WS_PATH,
                                   self._incoming,
                                   1,
                                   is_private=True)
            self.platform.register(self.OUTGOING_WS_PATH,
                                   self._outgoing,
                                   1,
                                   is_private=True)
            self.platform.register(self.SYSTEM_ALERT,
                                   self._alert,
                                   1,
                                   is_private=True)
            self.platform.register(self.SERVER_CONFIG,
                                   self._get_server_config,
                                   1,
                                   is_private=True)
            self._loop.create_task(worker())

    def close_connection(self, code, reason, stop_engine=False):
        async def async_close(rc, msg):
            if self.is_connected():
                # this only send a "closing signal" to the handler - it does not actually close the connection.
                self.close_code = rc
                self.close_message = msg
                await self.ws.close()

        def closing(rc, msg):
            self._loop.create_task(async_close(rc, msg))

        if stop_engine:
            self.normal = False
            self.cache.stop()
        self._loop.call_soon_threadsafe(closing, code, reason)

    async def connection_handler(self, url):
        try:
            async with aiohttp.ClientSession(
                    loop=self._loop,
                    timeout=aiohttp.ClientTimeout(total=10)) as session:
                full_path = url + '/' + self.origin
                self.ws = await session.ws_connect(full_path)
                envelope = EventEnvelope()
                envelope.set_to(self.INCOMING_WS_PATH).set_header(
                    'type', 'open')
                self.platform.send_event(envelope)
                self.log.info("Connected to " + full_path)
                closed = False
                self.last_active = time.time()

                while self.normal:
                    try:
                        msg = await self.ws.receive(timeout=1)
                    except asyncio.TimeoutError:
                        if not self.normal:
                            break
                        else:
                            # idle - send keep-alive
                            now = time.time()
                            if self.is_connected(
                            ) and now - self.last_active > 30:
                                self.last_active = now
                                self.send_keep_alive()
                            continue

                    # receive incoming event
                    self.last_active = time.time()
                    if msg.type == aiohttp.WSMsgType.TEXT:
                        if self.platform.has_route(self.INCOMING_WS_PATH):
                            envelope = EventEnvelope()
                            envelope.set_to(self.INCOMING_WS_PATH).set_header(
                                'type', 'text').set_body(msg.data)
                            self.platform.send_event(envelope)
                        else:
                            break
                    elif msg.type == aiohttp.WSMsgType.BINARY:
                        if self.platform.has_route(self.INCOMING_WS_PATH):
                            envelope = EventEnvelope()
                            envelope.set_to(self.INCOMING_WS_PATH).set_header(
                                'type', 'bytes').set_body(msg.data)
                            self.platform.send_event(envelope)
                        else:
                            break
                    else:
                        if msg.type == aiohttp.WSMsgType.ERROR:
                            self.log.error("Unexpected connection error")
                        if msg.type == aiohttp.WSMsgType.CLOSING:
                            # closing signal received - close the connection now
                            self.log.info("Disconnected, status=" +
                                          str(self.close_code) + ", message=" +
                                          self.close_message)
                            await self.ws.close(code=self.close_code,
                                                message=bytes(
                                                    self.close_message,
                                                    'utf-8'))
                            if self.platform.has_route(self.INCOMING_WS_PATH):
                                envelope = EventEnvelope()
                                envelope.set_to(self.INCOMING_WS_PATH).set_body(self.close_message)\
                                        .set_header('type', 'close').set_header('status', self.close_code)
                                self.platform.send_event(envelope)
                            closed = True
                        if msg.type == aiohttp.WSMsgType.CLOSE or msg.type == aiohttp.WSMsgType.CLOSED:
                            self.close_code = 1001 if msg.data is None else msg.data
                            self.close_message = 'OK' if msg.extra is None else str(
                                msg.extra)
                            self.log.info("Disconnected, status=" +
                                          str(self.close_code) + ", message=" +
                                          self.close_message)
                            if self.platform.has_route(self.INCOMING_WS_PATH):
                                envelope = EventEnvelope()
                                envelope.set_to(self.INCOMING_WS_PATH).set_body(self.close_message)\
                                        .set_header('type', 'close').set_header('status', self.close_code)
                                self.platform.send_event(envelope)
                            closed = True
                        break
                if not closed:
                    await self.ws.close(code=1000, message=b'OK')
                    self.ws = None
                    if self.platform.has_route(self.INCOMING_WS_PATH):
                        envelope = EventEnvelope()
                        envelope.set_to(self.INCOMING_WS_PATH).set_body('OK')\
                            .set_header('type', 'close').set_header('status', 1000)
                        self.platform.send_event(envelope)

        except aiohttp.ClientConnectorError:
            self._skip_url()
            self.log.warn("Unreachable " + url)
コード例 #2
0
class MultiLevelDict:
    def __init__(self, data=None):
        self.util = Utility()
        self.normalized = False
        self.dataset = dict() if data is None else data
        if not isinstance(self.dataset, dict):
            raise ValueError('Invalid input - Expect: dict, Actual: ' +
                             str(type(data)))

    def get_dict(self):
        return self.dataset

    @staticmethod
    def is_digits(n: str):
        for i in n:
            if i < '0' or i > '9':
                return False
        return True

    @staticmethod
    def is_list_element(item: str):
        return '[' in item and item.endswith(']') and (
            not item.startswith('['))

    def set_element(self,
                    composite_path: str,
                    value: any,
                    source_data: dict = None):
        if composite_path is None:
            raise ValueError('Missing composite_path')
        self.validate_composite_path_syntax(composite_path)
        data = self.dataset if source_data is None else source_data
        if not isinstance(data, dict):
            raise ValueError(
                f'Invalid input - Expect: dict, Actual: {type(data)}')
        segments = self.util.multi_split(composite_path, './')
        if len(segments) == 0:
            return
        current = data
        size = len(segments)
        n = 0
        composite = ''
        for p in segments:
            n += 1
            if self.is_list_element(p):
                sep = p.index('[')
                indexes = self._get_indexes(p[sep:])
                element = p[0:sep]
                parent = self.get_element(composite + element, source_data)
                if n == size:
                    if isinstance(parent, list):
                        self._set_list_element(indexes, parent, value)
                    else:
                        new_list = list()
                        self._set_list_element(indexes, new_list, value)
                        current[element] = new_list
                    break
                else:
                    if isinstance(parent, list):
                        next_dict = self.get_element(composite + p,
                                                     source_data)
                        if isinstance(next_dict, dict):
                            current = next_dict
                        else:
                            m = dict()
                            self._set_list_element(indexes, parent, m)
                            current = m
                    else:
                        next_map = dict()
                        new_list = list()
                        self._set_list_element(indexes, new_list, next_map)
                        current[element] = new_list
                        current = next_map
            else:
                if n == size:
                    current[p] = value
                    break
                else:
                    if p in current and isinstance(current[p], dict):
                        current = current[p]
                    else:
                        next_map = dict()
                        current[p] = next_map
                        current = next_map
            composite = composite + p + '.'

    def _set_list_element(self, indexes: list, source_data: list, value: any):
        current = self._expand_list(indexes, source_data)
        size = len(indexes)
        for i in range(0, size):
            idx = indexes[i]
            if i == size - 1:
                current[idx] = value
            else:
                o = current[idx]
                if isinstance(o, list):
                    current = o

    @staticmethod
    def _expand_list(indexes: list, source_data: list):
        current = source_data
        size = len(indexes)
        for i in range(0, size):
            idx = indexes[i]
            if idx >= len(current):
                diff = idx - len(current)
                while diff >= 0:
                    current.append(None)
                    diff -= 1
            if i == size - 1:
                break
            o = current[idx]
            if isinstance(o, list):
                current = o
            else:
                new_list = list()
                current[idx] = new_list
                current = new_list
        return source_data

    @staticmethod
    def _is_composite(path: str):
        return True if '.' in path or '/' in path or '[' in path or ']' in path else False

    def _get_indexes(self, index_segment: str):
        result = list()
        indexes = self.util.multi_split(index_segment, '[]')
        for i in indexes:
            if self.is_digits(i):
                result.append(int(i))
            else:
                result.append(-1)
        return result

    @staticmethod
    def _get_list_element(indexes: list, source_data: list):
        if (not isinstance(indexes, list)) or (not isinstance(source_data, list)) \
                or len(indexes) == 0 or len(source_data) == 0:
            return None
        current = source_data
        n = 0
        size = len(indexes)
        for i in indexes:
            n += 1
            if not isinstance(i, int):
                return None
            if i < 0 or i >= len(current):
                break
            o = current[i]
            if n == size:
                return o
            if isinstance(o, list):
                current = o
            else:
                break
        return None

    def get_element(self, composite_path: str, source_data: dict = None):
        if composite_path is None:
            return None
        data = self.dataset if source_data is None else source_data
        if not isinstance(data, dict):
            raise ValueError(
                f'Invalid input - Expect: dict, Actual: {type(data)}')
        if len(data) == 0:
            return None
        # special case for top level element that is using composite itself
        if composite_path in data:
            return data[composite_path]
        if not self._is_composite(composite_path):
            return None
        parts = self.util.multi_split(composite_path, './')
        current = dict(data)
        size = len(parts)
        n = 0
        for p in parts:
            n += 1
            if self.is_list_element(p):
                start = p.index('[')
                end = p.index(']', start)
                if end == -1:
                    break
                key = p[0:start]
                index = p[start + 1:end].strip()
                if len(index) == 0 or not self.is_digits(index):
                    break
                if key in current:
                    next_list = current[key]
                    if isinstance(next_list, list):
                        indexes = self._get_indexes(p[start:])
                        next_result = self._get_list_element(
                            indexes, next_list)
                        if n == size:
                            return next_result
                        if isinstance(next_result, dict):
                            current = next_result
                            continue
            else:
                if p in current:
                    next_dict = current[p]
                    if n == size:
                        return next_dict
                    elif isinstance(next_dict, dict):
                        current = next_dict
                        continue
            # item not found
            break
        return None

    def normalize_map(self):
        if not self.normalized:
            # do only once
            self.normalized = True
            flat_map = self.get_flat_map(self.dataset)
            result = dict()
            for k in flat_map:
                self.set_element(k, flat_map[k], result)
            self.dataset = result

    def get_flat_map(self, data: dict = None):
        if not isinstance(data, dict):
            raise ValueError(
                f'Invalid input - Expect: dict, Actual: {type(data)}')
        result = dict()
        self._get_flat_map(None, data, result)
        return result

    def _get_flat_map(self, prefix: any, src: dict, target: dict):
        for k in src:
            v = src[k]
            key = k if prefix is None else prefix + "." + k
            if isinstance(v, dict):
                self._get_flat_map(key, v, target)
            elif isinstance(v, list):
                self._get_flat_list(key, v, target)
            else:
                target[key] = v

    def _get_flat_list(self, prefix: str, src: list, target: dict):
        n = 0
        for v in src:
            key = prefix + "[" + str(n) + "]"
            n += 1
            if isinstance(v, dict):
                self._get_flat_map(key, v, target)
            elif isinstance(v, list):
                self._get_flat_list(key, v, target)
            else:
                target[key] = v

    def validate_composite_path_syntax(self, path: str):
        segments = self.util.multi_split(path, './')
        if len(segments) == 0:
            raise ValueError('Missing composite path')
        for s in segments:
            if '[' in s or ']' in s:
                if '[' not in s:
                    raise ValueError(
                        'Invalid composite path - missing start bracket')
                if not s.endswith(']'):
                    raise ValueError(
                        'Invalid composite path - missing end bracket')
                sep1 = s.index('[')
                sep2 = s.index(']')
                if sep2 < sep1:
                    raise ValueError(
                        'Invalid composite path - missing start bracket')
                start = False
                for c in s[sep1:]:
                    if c == '[':
                        if start:
                            raise ValueError(
                                'Invalid composite path - missing end bracket')
                        else:
                            start = True
                    elif c == ']':
                        if not start:
                            raise ValueError(
                                'Invalid composite path - duplicated end bracket'
                            )
                        else:
                            start = False
                    else:
                        if start:
                            if c < '0' or c > '9':
                                raise ValueError(
                                    'Invalid composite path - indexes must be digits'
                                )
                        else:
                            raise ValueError(
                                'Invalid composite path - invalid indexes')
コード例 #3
0
class NetworkConnector:

    INCOMING_WS_PATH = "ws.incoming"
    OUTGOING_WS_PATH = "ws.outgoing"
    SYSTEM_ALERT = "system.alerts"
    SERVER_CONFIG = "system.config"
    MAX_PAYLOAD = "max.payload"
    TRACE_AGGREGATION = "trace.aggregation"
    DISTRIBUTED_TRACING = "distributed.tracing"
    CONNECTOR_LIFECYCLE = 'cloud.connector.lifecycle'
    # payload segmentation reserved tags (from v1.13.0 onwards)
    MSG_ID = '_id_'
    COUNT = '_blk_'
    TOTAL = '_max_'

    def __init__(self, platform, distributed_trace, loop, url_list, origin):
        self.platform = platform
        self._subscription = list()
        self._distributed_trace = distributed_trace
        self._loop = loop
        self.log = platform.log
        self.config = platform.config
        self.normal = True
        self.started = False
        self.ready = False
        self.ws = None
        self.close_code = 1000
        self.close_message = 'OK'
        self.last_active = time.time()
        self.max_ws_payload = 32768
        self.util = Utility()
        self.urls = self.util.multi_split(url_list, ', ')
        self.next_url = 1
        self.origin = origin
        self.cache = SimpleCache(loop, self.log, timeout_seconds=30)
        self.api_key = self._get_api_key()

    def _get_api_key(self):
        api_key_env_var = self.config.get_property(
            'language.pack.key', default_value='LANGUAGE_PACK_KEY')
        if api_key_env_var in os.environ:
            self.log.info(
                f'Found API key in environment variable {api_key_env_var}')
            return os.environ[api_key_env_var]
        # check temp file system because API key not in environment
        temp_dir = '/tmp/config'
        if not os.path.exists(temp_dir):
            os.makedirs(temp_dir, exist_ok=True)
            self.log.info(f'Folder {temp_dir} created')
        api_key_file = temp_dir + "/lang-api-key.txt"
        if os.path.exists(api_key_file):
            with open(api_key_file) as f:
                self.log.info(f'Reading language API key from {api_key_file}')
                return f.read().strip()
        else:
            with open(api_key_file, 'w') as f:
                self.log.info(
                    f'Generating new language API key in {api_key_file}')
                value = ''.join(str(uuid.uuid4()).split('-'))
                f.write(value + '\n')
                return value

    def _get_next_url(self):
        # index starts from 1
        return self.urls[self.next_url - 1]

    def _skip_url(self):
        self.next_url += 1
        if self.next_url > len(self.urls):
            self.next_url = 1

    def send_keep_alive(self):
        message = "Keep-Alive " + self.util.get_iso_8601(time.time(),
                                                         show_ms=True)
        envelope = EventEnvelope()
        envelope.set_to(self.OUTGOING_WS_PATH).set_header(
            'type', 'text').set_body(message)
        self.platform.send_event(envelope)

    def send_payload(self, data: dict):
        if 'type' in data and data['type'] == 'event' and 'event' in data:
            evt = data['event']
            payload = msgpack.packb(evt, use_bin_type=True)
            payload_len = len(payload)
            if payload_len > self.max_ws_payload:
                msg_id = evt['id']
                total = int(payload_len / self.max_ws_payload)
                if payload_len > total:
                    total += 1
                buffer = io.BytesIO(payload)
                count = 0
                for i in range(total):
                    count += 1
                    block = EventEnvelope()
                    block.set_header(self.MSG_ID, msg_id)
                    block.set_header(self.COUNT, count)
                    block.set_header(self.TOTAL, total)
                    block.set_body(buffer.read(self.max_ws_payload))
                    block_map = dict()
                    block_map['type'] = 'block'
                    block_map['block'] = block.to_map()
                    block_payload = msgpack.packb(block_map, use_bin_type=True)
                    envelope = EventEnvelope()
                    envelope.set_to(self.OUTGOING_WS_PATH).set_header(
                        'type', 'bytes').set_body(block_payload)
                    self.platform.send_event(envelope)
            else:
                relay_map = dict()
                relay_map['type'] = 'event'
                relay_map['event'] = payload
                envelope = EventEnvelope()
                envelope_payload = msgpack.packb(relay_map, use_bin_type=True)
                envelope.set_to(self.OUTGOING_WS_PATH).set_header(
                    'type', 'bytes').set_body(envelope_payload)
                self.platform.send_event(envelope)
        else:
            envelope = EventEnvelope()
            envelope_payload = msgpack.packb(data, use_bin_type=True)
            envelope.set_to(self.OUTGOING_WS_PATH).set_header(
                'type', 'bytes').set_body(envelope_payload)
            self.platform.send_event(envelope)

    def _get_server_config(self, headers: dict, body: any):
        if 'type' in headers:
            # at this point, login is successful
            if headers['type'] == 'system.config' and isinstance(body, dict):
                if self.MAX_PAYLOAD in body:
                    self.max_ws_payload = body[self.MAX_PAYLOAD]
                    self.log.info('Authenticated')
                    self._send_life_cycle_event({'type': 'authenticated'})
                    self.log.info(
                        f'Automatic payload segmentation at {format(self.max_ws_payload, ",d")} bytes'
                    )
                if self.TRACE_AGGREGATION in body:
                    self.platform.set_trace_support(
                        body[self.TRACE_AGGREGATION])
                # advertise public routes to language connector
                for r in self.platform.get_routes('public'):
                    self.send_payload({'type': 'add', 'route': r})
                # tell server that I am ready
                self.send_payload({'type': 'ready'})
            # server acknowledges my ready signal
            if headers['type'] == 'ready':
                self.ready = True
                self.log.info('Ready')
                self._send_life_cycle_event({'type': 'ready'})

    def subscribe_life_cycle(self, callback: str):
        if not isinstance(callback, str):
            raise ValueError('callback route name must be str')
        if callback not in self._subscription:
            self._subscription.append(callback)

    def unsubscribe_life_cycle(self, callback: str):
        if not isinstance(callback, str):
            raise ValueError('callback route name must be str')
        if callback in self._subscription:
            self._subscription.remove(callback)

    def _send_life_cycle_event(self, headers: dict):
        event = EventEnvelope()
        event.set_to(self.CONNECTOR_LIFECYCLE).set_headers(headers)
        self.platform.send_event(event)

    def _life_cycle(self, headers: dict, body: any):
        for subscriber in self._subscription:
            try:
                event = EventEnvelope()
                event.set_to(subscriber).set_headers(headers).set_body(body)
                self.platform.send_event(event)
            except ValueError as e:
                self.log.warn(
                    f'Unable to relay life cycle event {headers} to {subscriber} - {e}'
                )

    def _alert(self, headers: dict, body: any):
        if 'status' in headers:
            if headers['status'] == '200':
                self.log.info(str(body))
            else:
                self.log.warn(str(body) + ", status=" + headers['status'])

    def _incoming(self, headers: dict, body: any):
        """
        This function handles incoming messages from the websocket connection with the Mercury language connector.
        It must be invoked using events. It should not be called directly to guarantee proper event sequencing.

        Args:
            headers: type is open, close, text or bytes
            body: string or bytes

        Returns: None

        """
        if self.ws and 'type' in headers:
            if headers['type'] == 'open':
                self.ready = False
                self.log.info("Login to language connector")
                self.send_payload({'type': 'login', 'api_key': self.api_key})
                self._send_life_cycle_event(headers)
            if headers['type'] == 'close':
                self.ready = False
                self.log.info("Closed")
                self._send_life_cycle_event(headers)
            if headers['type'] == 'text':
                self.log.debug(body)
            if headers['type'] == 'bytes':
                event = msgpack.unpackb(body, raw=False)
                if 'type' in event:
                    event_type = event['type']
                    if event_type == 'block' and 'block' in event:
                        envelope = EventEnvelope()
                        inner_event = envelope.from_map(event['block'])
                        inner_headers = inner_event.get_headers()
                        if self.MSG_ID in inner_headers and self.COUNT in inner_headers and self.TOTAL in inner_headers:
                            msg_id = inner_headers[self.MSG_ID]
                            msg_count = inner_headers[self.COUNT]
                            msg_total = inner_headers[self.TOTAL]
                            data = inner_event.get_body()
                            if isinstance(data, bytes):
                                buffer = self.cache.get(msg_id)
                                if buffer is None:
                                    buffer = io.BytesIO()
                                buffer.write(data)
                                if msg_count == msg_total:
                                    self.cache.remove(msg_id)
                                    # reconstruct event for processing
                                    buffer.seek(0)
                                    envelope = EventEnvelope()
                                    unpacked = msgpack.unpackb(buffer.read(),
                                                               raw=False)
                                    restored = envelope.from_map(unpacked)
                                    target = restored.get_to()
                                    if self.platform.has_route(target):
                                        self.platform.send_event(restored)
                                    else:
                                        self.log.warn(
                                            f'Incoming event dropped because {target} not found'
                                        )
                                else:
                                    self.cache.put(msg_id, buffer)
                    if event_type == 'event' and 'event' in event:
                        unpacked = msgpack.unpackb(event['event'], raw=False)
                        envelope = EventEnvelope()
                        inner_event = envelope.from_map(unpacked)
                        if self.platform.has_route(inner_event.get_to()):
                            self.platform.send_event(inner_event)
                        else:
                            self.log.warn(
                                f'Incoming event dropped because {inner_event.get_to()} not found'
                            )

    def _outgoing(self, headers: dict, body: any):
        """
        This function handles sending outgoing messages to the websocket connection with the Mercury language connector.
        It must be invoked using events. It should not be called directly to guarantee proper event sequencing.

        Args:
            headers: type is close, text or bytes
            body: string or bytes

        Returns: None

        """
        if 'type' in headers:
            if headers['type'] == 'close':
                code = 1000 if 'code' not in headers else headers['code']
                reason = 'OK' if 'reason' not in headers else headers['reason']
                self.close_connection(code, reason)
            if headers['type'] == 'text':
                self._send_text(body)
            if headers['type'] == 'bytes':
                self._send_bytes(body)

    def _send_text(self, body: str):
        def send(data: str):
            async def async_send(d: str):
                await self.ws.send_str(d)

            self._loop.create_task(async_send(data))

        if self.is_connected():
            self._loop.call_soon_threadsafe(send, body)

    def _send_bytes(self, body: bytes):
        def send(data: bytes):
            async def async_send(d: bytes):
                await self.ws.send_bytes(d)

            self._loop.create_task(async_send(data))

        if self.is_connected():
            self._loop.call_soon_threadsafe(send, body)

    def is_connected(self):
        return self.started and self.ws

    def is_ready(self):
        return self.is_connected() and self.ready

    def start_connection(self):
        async def worker():
            while self.normal:
                await self._loop.create_task(
                    self.connection_handler(self._get_next_url()))
                # check again because the handler may have run for a while
                if self.normal:
                    # retry connection in 5 seconds
                    for _ in range(10):
                        await asyncio.sleep(0.5)
                        if not self.normal:
                            break
                else:
                    break

        if not self.started:
            self.started = True
            self.platform.register(self.DISTRIBUTED_TRACING,
                                   self._distributed_trace.logger,
                                   1,
                                   is_private=True)
            self.platform.register(self.INCOMING_WS_PATH,
                                   self._incoming,
                                   1,
                                   is_private=True)
            self.platform.register(self.OUTGOING_WS_PATH,
                                   self._outgoing,
                                   1,
                                   is_private=True)
            self.platform.register(self.SYSTEM_ALERT,
                                   self._alert,
                                   1,
                                   is_private=True)
            self.platform.register(self.SERVER_CONFIG,
                                   self._get_server_config,
                                   1,
                                   is_private=True)
            self.platform.register(self.CONNECTOR_LIFECYCLE,
                                   self._life_cycle,
                                   1,
                                   is_private=True)
            self._loop.create_task(worker())

    def close_connection(self, code, reason, stop_engine=False):
        async def async_close(rc, msg):
            if self.is_connected():
                # this only send a "closing signal" to the handler - it does not actually close the connection.
                self.close_code = rc
                self.close_message = msg
                await self.ws.close()

        def closing(rc, msg):
            self._loop.create_task(async_close(rc, msg))

        if stop_engine:
            self.normal = False
            self.cache.stop()
        self._loop.call_soon_threadsafe(closing, code, reason)

    async def connection_handler(self, url):
        try:
            async with aiohttp.ClientSession(
                    loop=self._loop,
                    timeout=aiohttp.ClientTimeout(total=10)) as session:
                full_path = f'{url}/{self.origin}'
                self.ws = await session.ws_connect(full_path)
                envelope = EventEnvelope()
                envelope.set_to(self.INCOMING_WS_PATH).set_header(
                    'type', 'open').set_header('url', full_path)
                self.platform.send_event(envelope)
                self.log.info(f'Connected to {full_path}')
                closed = False
                self.last_active = time.time()

                while self.normal:
                    try:
                        msg = await self.ws.receive(timeout=1)
                    except asyncio.TimeoutError:
                        if not self.normal:
                            break
                        else:
                            # idle - send keep-alive
                            now = time.time()
                            if self.is_connected(
                            ) and now - self.last_active > 30:
                                self.last_active = now
                                self.send_keep_alive()
                            continue

                    # receive incoming event
                    self.last_active = time.time()
                    if msg.type == aiohttp.WSMsgType.TEXT:
                        if self.platform.has_route(self.INCOMING_WS_PATH):
                            envelope = EventEnvelope()
                            envelope.set_to(self.INCOMING_WS_PATH).set_header(
                                'type', 'text').set_body(msg.data)
                            self.platform.send_event(envelope)
                        else:
                            break
                    elif msg.type == aiohttp.WSMsgType.BINARY:
                        if self.platform.has_route(self.INCOMING_WS_PATH):
                            envelope = EventEnvelope()
                            envelope.set_to(self.INCOMING_WS_PATH).set_header(
                                'type', 'bytes').set_body(msg.data)
                            self.platform.send_event(envelope)
                        else:
                            break
                    else:
                        if msg.type == aiohttp.WSMsgType.ERROR:
                            self.log.error('Unexpected connection error')
                        if msg.type == aiohttp.WSMsgType.CLOSING:
                            # closing signal received - close the connection now
                            self.log.info(
                                f'Disconnected, status={self.close_code}, message={self.close_message}'
                            )
                            await self.ws.close(code=self.close_code,
                                                message=bytes(
                                                    self.close_message,
                                                    'utf-8'))
                            if self.platform.has_route(self.INCOMING_WS_PATH):
                                envelope = EventEnvelope()
                                envelope.set_to(self.INCOMING_WS_PATH).set_body(self.close_message)\
                                        .set_header('type', 'close').set_header('status', self.close_code)
                                self.platform.send_event(envelope)
                            closed = True
                        if msg.type == aiohttp.WSMsgType.CLOSE or msg.type == aiohttp.WSMsgType.CLOSED:
                            self.close_code = 1001 if msg.data is None else msg.data
                            self.close_message = 'OK' if msg.extra is None else str(
                                msg.extra)
                            self.log.info(
                                f'Disconnected, status={self.close_code}, message={self.close_message}'
                            )
                            if self.platform.has_route(self.INCOMING_WS_PATH):
                                envelope = EventEnvelope()
                                envelope.set_to(self.INCOMING_WS_PATH).set_header('message', self.close_message) \
                                        .set_header('type', 'close').set_header('status', self.close_code)
                                self.platform.send_event(envelope)
                            closed = True
                        break
                if not closed:
                    await self.ws.close(code=1000, message=b'OK')
                    self.ws = None
                    if self.platform.has_route(self.INCOMING_WS_PATH):
                        envelope = EventEnvelope()
                        envelope.set_to(self.INCOMING_WS_PATH).set_body('OK')\
                            .set_header('type', 'close').set_header('status', 1000)
                        self.platform.send_event(envelope)

        except aiohttp.ClientConnectorError:
            self._skip_url()
            self.log.warn(f'Unreachable {url}')