iscp._upstream のソースコード

import asyncio
import contextlib
import copy
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Awaitable, Callable, Coroutine, Dict, List, Optional, Set
from uuid import UUID

from . import _exceptions as exceptions
from . import _message as message
from . import _model
from ._flush_policy import FlushPolicy
from ._sequence import Sequence

__all__ = [
    "Upstream",
    "AfterReceiveAckCallback",
    "BeforeSendDataPointsCallback",
    "UpstreamClosedEventHandler",
    "UpstreamClosedEvent",
]

logger = logging.getLogger(__name__)

maxUint64 = 18446744073709551615
maxUint32 = 4294967295


@dataclass
class UpstreamClosedEvent:
    """
    アップストリームをクローズした時のイベントです。

    Attributes:
        state(iscp.UpstreamState): クローズした時のアップストリームの状態
        error(Optional[iscp.ISCPException]): 内部エラーが発生した場合の例外
    """

    state: _model.UpstreamState
    error: Optional[exceptions.ISCPException] = None


UpstreamClosedEventHandler = Callable[[UpstreamClosedEvent], None]

Requester = Callable[[message.RequestMessage], Awaitable[message.RequestMessage]]
Sender = Callable[[message.UpstreamChunk], Awaitable[None]]

BeforeSendDataPointsCallback = Callable[[UUID, _model.UpstreamChunk], None]
AfterReceiveAckCallback = Callable[[UUID, _model.UpstreamChunkAck], None]


[ドキュメント]class Upstream(object): """ アップストリームを表すクラスです。 .. attention:: このクラスのオブジェクトは、必ず iscp.Conn の `open_upstream()` を使用して生成してください。 .. code-block:: python async with await Conn.connect(...) as conn: async with await conn.open_upstream(...) as upstream: pass このコードは以下のコードと等価です。 .. code-block:: python async with await Conn.connect(...) as conn: upstream = await conn.open_upstream(...) try: pass finally: await upstream.close() """ def __init__( self, id: UUID, id_alias: int, session_id: str, data_id_aliases: Dict[int, message.DataID], server_time: message.DateTime, requester: Requester, sender: Sender, close_timeout: float, ack_interval: float, expiry_interval: float, qos: message.QoS, flush_policy: FlushPolicy, persist: bool, receive_ack_callback: Optional[AfterReceiveAckCallback] = None, send_data_point_callback: Optional[BeforeSendDataPointsCallback] = None, closed_event_handler: Optional[UpstreamClosedEventHandler] = None, close_session: bool = False, ): self._flush_policy = flush_policy self._close_timeout = close_timeout self._persist = persist self._close_session = close_session # From Upstream Open Request self._session_id = session_id self._ack_interval = ack_interval self._expiry_interval = expiry_interval self._qos = qos # From Upstream Open Response self._id = id self._id_alias = id_alias self._data_id_aliases = data_id_aliases self._rev_data_id_aliases = {v: k for k, v in self._data_id_aliases.items()} self._server_time = server_time # State self._total_data_points = 0 self._final_sequence_number = 0 self._waiting_sequence_numbers: Set[int] = set() # Function self._requester = requester self._sender = sender self._sequence_number_sequence = Sequence.for_sequence_number() # Callback self._send_data_point_callback = send_data_point_callback if send_data_point_callback is not None else lambda a, b: None self._receive_ack_callback = receive_ack_callback if receive_ack_callback is not None else lambda a, b: None # Internal Event self._started_draining = asyncio.Event() self._received_all_ack = asyncio.Event() self._closed = asyncio.Event() self._finished_flush_buffer_size_loop = asyncio.Event() self._finished_flush_interval_loop = asyncio.Event() self._closed_event_handler = closed_event_handler # Internal Queue self._ack_queue: asyncio.Queue[message.UpstreamChunkAck] = asyncio.Queue() self._receive_ack_callback_queue: asyncio.Queue[_model.UpstreamChunkAck] = asyncio.Queue() self._send_data_point_callback_queue: asyncio.Queue[_model.UpstreamChunk] = asyncio.Queue() # Others self._send_buffer_condition = asyncio.Condition() self._send_buffer: Dict[message.DataID, List[message.DataPoint]] = defaultdict(lambda: []) self._send_buffer_size: int = 0 self._all_loops: List[asyncio.Task] = [] self._internal_error: Optional[exceptions.ISCPException] = None @property def id(self): """ ストリームIDを取得します。 Returns: UUID: ストリームID """ return self._id @property def server_time(self): """ UpstreamResponseで返却されたサーバー時刻を取得します。 Returns: iscp.DateTime: UpstreamResponseで返却されたサーバー時刻 """ return self._server_time @property def state(self): """ ストリームの状態を取得します。 Returns: UpstreamState: アップストリームの状態 """ return _model.UpstreamState( data_points_buffer=[ _model.DataPointGroup( data_id=copy.deepcopy(k), data_points=copy.deepcopy(v), ) for k, v in self._send_buffer.items() ], data_id_aliases=self._data_id_aliases.copy(), total_data_points=self._total_data_points, last_issued_sequence_number=self._sequence_number_sequence._current, ) @property def session_id(self): """ セッションIDを取得します。 Returns: str: セッションID """ return self._session_id @property def ack_interval(self): """ Ackの返却間隔を取得します。 Returns: float: Ackの返却間隔(秒) """ return self._ack_interval @property def expiry_interval(self): """ 有効期限を取得します。 Returns: float: ストリームの有効期限(秒) """ return self._expiry_interval @property def qos(self): """ QoSを取得します。 Returns: iscp.QoS: QoS """ return self._qos @property def persist(self): """ 永続化フラグを取得します。 Returns: Bool: 永続化フラグ """ return self._persist @property def flush_policy(self): """ フラッシュポリシーを取得します。 Returns: iscp.FlushPolicy: フラッシュポリシー """ return self._flush_policy @property def close_timeout(self): """ クローズタイムアウト(秒)を取得します。 Returns: float: クローズタイムアウト(秒) """ return self._close_timeout @property def close_session(self): """ クローズ時にセッションをクローズするかどうかを取得します。 Returns: bool: クローズ時にセッションをクローズするかどうか """ return self._close_session async def _start(self): self._all_loops.append(asyncio.create_task(self._ack_loop())) self._all_loops.append(asyncio.create_task(self._flush_interval_loop())) self._all_loops.append(asyncio.create_task(self._flush_buffer_size_loop())) self._all_loops.append(asyncio.create_task(self._receive_ack_hook_loop())) self._all_loops.append(asyncio.create_task(self._send_data_point_hook_loop())) asyncio.create_task(self._wait_closed_for_event_handler()) return self async def _wait_closed_for_event_handler(self): await self._closed.wait() if self._closed_event_handler: self._closed_event_handler( UpstreamClosedEvent( state=self.state, error=self._internal_error, ) ) async def _receive_ack_hook_loop(self): logger.debug("start receive_ack_hook_loop") while True: msg = await self._receive_ack_callback_queue.get() self._receive_ack_callback(self._id, msg) self._receive_ack_callback_queue.task_done() async def _send_data_point_hook_loop(self): logger.debug("start send_data_point_hook_loop") while not self._started_draining.is_set(): msg = await self._send_data_point_callback_queue.get() self._send_data_point_callback(self._id, msg) self._send_data_point_callback_queue.task_done() async def _ack_loop(self): try: while True: async with _event_or_coro(self._ack_queue.get(), self._closed) as (done, pending): for result in done: ack = await result if isinstance(ack, message.UpstreamChunkAck): self._data_id_aliases.update(ack.data_id_aliases) await self._receive_ack_callback_queue.put( _model.UpstreamChunkAck( results=[ _model.UpstreamChunkResult( sequence_number=v.sequence_number, result_code=v.result_code, result_string=v.result_string, ) for v in ack.results ] ) ) self._update_data_id_alias(ack.data_id_aliases) for n in [n.sequence_number for n in ack.results]: self._waiting_sequence_numbers.remove(n) self._ack_queue.task_done() if self._finished_flush_buffer_size_loop.is_set() and self._finished_flush_interval_loop.is_set(): if len(self._waiting_sequence_numbers) == 0: self._received_all_ack.set() return else: return except Exception as e: logger.error(e) def _update_data_id_alias(self, arg: Dict[int, message.DataID]): self._data_id_aliases.update(arg) self._rev_data_id_aliases = {v: k for k, v in self._data_id_aliases.items()} async def _flush_buffer_size_loop(self): try: while True: async with self._send_buffer_condition: if self._closed.is_set(): return if self._started_draining.is_set() or self._flush_policy.IsFlush(self._send_buffer_size): await self._flush() if self._started_draining.is_set(): return await self._send_buffer_condition.wait() finally: self._finished_flush_buffer_size_loop.set() async def _flush_interval_loop(self): try: tick = self._flush_policy.Ticker() if not tick: return async for _ in tick(): async with self._send_buffer_condition: if self._started_draining.is_set() and not self._send_buffer: return await self._flush() if self._started_draining.is_set(): return await self._send_buffer_condition.wait() finally: if tick: tick.stop() self._finished_flush_interval_loop.set()
[ドキュメント] async def flush(self): """ データポイントの内部バッファをUpstreamChunkとしてサーバーへ送信します。 """ await self._flush()
async def _flush(self): if len(self._send_buffer) == 0: return data_ids: List[message.DataID] = [] data_point_groups_msg: List[message.DataPointGroup] = [] data_point_groups: List[_model.DataPointGroup] = [] try: for k, v in self._send_buffer.items(): data_point_groups.append(_model.DataPointGroup(data_id=k, data_points=v)) self._total_data_points += len(v) if self._total_data_points > maxUint64: raise exceptions.ISCPException(message="maximum total_data_points exceeded") if k not in self._rev_data_id_aliases: data_ids.append(k) data_point_groups_msg.append(message.DataPointGroup(data_id_or_alias=k, data_points=v)) continue data_point_groups_msg.append(message.DataPointGroup(data_id_or_alias=self._rev_data_id_aliases[k], data_points=v)) sequence_number = self._sequence_number_sequence() chunk = message.UpstreamChunk( stream_id_alias=self._id_alias, stream_chunk=message.StreamChunk( sequence_number=sequence_number, data_point_groups=data_point_groups_msg, ), data_ids=data_ids, extension_fields=message.UpstreamChunkExtensionFields(), ) self._final_sequence_number = chunk.stream_chunk.sequence_number self._waiting_sequence_numbers.add(chunk.stream_chunk.sequence_number) await self._send_data_point_callback_queue.put( _model.UpstreamChunk( sequence_number=chunk.stream_chunk.sequence_number, data_point_groups=data_point_groups, ) ) await self._sender(chunk) self._send_buffer = defaultdict(lambda: []) self._send_buffer_size = 0 except exceptions.ISCPException as e: self._internal_error = e self._closed.set() for t in self._all_loops: t.cancel() await asyncio.wait(self._all_loops) resp = await self._requester( message.UpstreamCloseRequest( request_id=0, stream_id=self._id, total_data_points=self._total_data_points, final_sequence_number=self._final_sequence_number, extension_fields=message.UpstreamCloseRequestExtensionFields( close_session=self._close_session, ), ) ) if isinstance(resp, message.UpstreamCloseResponse): if resp.result_code is message.ResultCode.SUCCEEDED: return logger.error( f"failed to close a upstream. stream_id \ [{self._id}] code=[{resp.result_code}] msg=[{resp.result_string}]", ) return logger.error(exceptions.ISCPFailedMessageError(received_message=resp)) async def __aenter__(self): return self async def __aexit__(self, exc_type, exc, tb): with contextlib.suppress(exceptions.ISCPException): await self.close()
[ドキュメント] async def write_data_points(self, data_id: message.DataID, *data_points: message.DataPoint): """ データポイントを送信します。 Args: data_id(iscp.DataID): データID *data_points(iscp.DataPoint): データポイント """ if self._started_draining.is_set(): raise exceptions.ISCPTransportClosedError() async with self._send_buffer_condition: self._send_buffer[data_id].extend(data_points) self._send_buffer_size += sum([len(v.payload) for v in data_points]) self._send_buffer_condition.notify_all()
async def _handle_ack(self, msg: message.UpstreamChunkAck): await self._ack_queue.put(msg)
[ドキュメント] async def close(self, overwrite_close_session: Optional[bool] = None): """ アップストリームを閉じます。 Args: overwrite_close_session(Optional[bool]): アップストリームに設定されている `close_session` を上書きします。 """ if self._closed.is_set(): return if self._started_draining.is_set(): raise exceptions.ISCPException("already draining") self._started_draining.set() try: while ( self._send_buffer or not self._finished_flush_buffer_size_loop.is_set() or not self._finished_flush_interval_loop.is_set() ): async with self._send_buffer_condition: self._send_buffer_condition.notify_all() await asyncio.sleep(0) if len(self._waiting_sequence_numbers) != 0: await asyncio.wait_for(self._received_all_ack.wait(), timeout=self._close_timeout) except asyncio.TimeoutError as e: logger.warning(e) try: for t in self._all_loops: t.cancel() await asyncio.wait(self._all_loops) resp = await self._requester( message.UpstreamCloseRequest( request_id=0, stream_id=self._id, total_data_points=self._total_data_points, final_sequence_number=self._final_sequence_number, extension_fields=message.UpstreamCloseRequestExtensionFields( close_session=overwrite_close_session if overwrite_close_session else self._close_session ), ) ) if isinstance(resp, message.UpstreamCloseResponse): if resp.result_code is message.ResultCode.SUCCEEDED: return logger.error( f"failed to close a upstream. stream_id \ [{self._id}] code=[{resp.result_code}] msg=[{resp.result_string}]", ) return raise exceptions.ISCPFailedMessageError(received_message=resp) finally: self._closed.set()
@contextlib.asynccontextmanager async def _event_or_coro(aw: Coroutine, *evs: asyncio.Event): ts = [asyncio.create_task(ev.wait()) for ev in evs] ts.append(asyncio.create_task(aw)) try: yield await asyncio.wait(ts, return_when=asyncio.FIRST_COMPLETED) finally: for t in ts: t.cancel()