示例#1
0
    async def _send_loop(self, sock):
        """Continously send packages to the connection partner.

        This coroutine, once spawned, will keep sending packages to the remote_address until it is explicitly
        cancelled or the connection times out.

        # Arguments
        sock (curio.io.Socket): socket via which to send the packages

        """
        logger.debug(
            f"Starting to send packages to {self.remote_address} every {self._package_interval} seconds."
        )
        congestion_avoidance_task = await curio.spawn(
            self._congestion_avoidance_monitor)
        while True:
            try:
                t0 = time.time()
                if t0 - self._last_recv > self._timeout:
                    logger.warning(
                        f"Connection to {self.remote_address} timed out after {self._timeout} seconds."
                    )
                    self._set_status("Disconnected")
                    break
                await self._send_next_package(sock)
                await curio.sleep(
                    max([self._package_interval - time.time() + t0, 0]))
            except curio.CancelledError:
                break
        logger.debug(f"Stopped sending packages to {self.remote_address}.")
        await congestion_avoidance_task.cancel()
示例#2
0
    async def _send_next_package(self, sock):
        """Send a package with up to 5 events.

        This coroutine returns once the package is sent.

        # Arguments
        sock (curio.io.Socket): socket via which to send the package

        """
        self.local_sequence += 1
        package = self._create_next_package()
        while len(
                package.events) < 5 and not self._outgoing_event_queue.empty():
            event, callback_sequence = await self._outgoing_event_queue.get()
            if callback_sequence != 0:
                if self.local_sequence not in self._events_with_callbacks:
                    self._events_with_callbacks[self.local_sequence] = [
                        callback_sequence
                    ]
                else:
                    self._events_with_callbacks[self.local_sequence].append(
                        callback_sequence)
            logger.debug((
                f"Sending event of type {event.type} to {self.remote_address}."
                f"event data: handler_args = {event.handler_args}, handler_kwargs = {event.handler_kwargs}"
            ))
            package.add_event(event)
            await self._outgoing_event_queue.task_done()
        await sock.sendto(package.to_datagram(), self.remote_address)
        logger.debug(
            f"Sent package with sequence number {package.header.sequence} to {self.remote_address}."
        )
        self._pending_acks[package.header.sequence] = time.time()
示例#3
0
    def dispatch_event(self,
                       event: Event,
                       ack_callback=None,
                       timeout_callback=None):
        """Send an event to the connection partner.

        # Arguments
        event (pygase.event.Event): the event to dispatch
        ack_callback (callable, coroutine): will be executed after the event was received
        timeout_callback (callable, coroutine): will be executed if the event was not received

        ---
        Using long-running blocking operations in any of the callback functions can disturb the connection.

        """
        callback_sequence = 0
        if ack_callback is not None or timeout_callback is not None:
            self._event_callback_sequence += 1
            callback_sequence = self._event_callback_sequence
            self._event_callbacks[self._event_callback_sequence] = {
                "ack": ack_callback,
                "timeout": timeout_callback
            }
        self._outgoing_event_queue.put((event, callback_sequence))
        logger.debug(
            f"Dispatched event of type {event.type} to be sent to {self.remote_address}."
        )
示例#4
0
 def __init__(self, game_state_store: GameStateStore):
     logger.debug("Creating GameStateMachine instance.")
     self.game_time: float = 0.0
     self._event_queue = curio.UniversalQueue()
     self._universal_event_handler = UniversalEventHandler()
     self._game_state_store = game_state_store
     self._game_loop_is_running = False
示例#5
0
 def __init__(self, game_state_store: GameStateStore):
     logger.debug("Creating Server instance.")
     self.connections: dict = {}
     self.host_client: tuple = None
     self.game_state_store = game_state_store
     self._universal_event_handler = UniversalEventHandler()
     self._hostname: str = None
     self._port: int = None
示例#6
0
 async def _recv(self, package: ServerPackage):
     """Extend #Connection._recv to update the game state."""
     await super()._recv(package)
     async with curio.abide(self.game_state_context.lock):
         logger.debug((f"Updating game state from time order "
                       f"{self.game_state_context.ressource.time_order} to "
                       f"{package.game_state_update.time_order}."))
         self.game_state_context.ressource += package.game_state_update
示例#7
0
 async def shutdown(self, shutdown_server: bool = False):  # pylint: disable=function-redefined
     # pylint: disable=missing-docstring
     if shutdown_server:
         await self._command_queue.put("shutdown")
     else:
         await self._command_queue.put("shut_me_down")
     logger.debug((
         f"Dispatched shutdown command with shutdown_server={shutdown_server} "
         f"for connection to {self.remote_address}."))
示例#8
0
 def __init__(self, initial_game_state: GameState = None):
     logger.debug("Creating GameStateStore instance.")
     self._game_state = initial_game_state if initial_game_state is not None else GameState(
     )
     if not isinstance(self._game_state, GameState):
         raise TypeError(
             f"'initial_game_state' should be of type 'GameState', not '{self._game_state.__class__.__name__}'."
         )
     self._game_state_update_cache = [GameStateUpdate(0)]
示例#9
0
    def _push_event(self, event: Event) -> None:
        """Push an event into the state machines event queue.

        This method can be spawned as a coroutine.

        """
        logger.debug(
            f"State machine receiving event of type {event.type} via event wire."
        )
        self._event_queue.put(event)
示例#10
0
    async def _event_loop(self):
        """Continously handle incoming events.

        This coroutine, once spawned, will keep handling events until it is explicitly cancelled.

        """
        logger.debug(
            f"Starting event loop for connection to {self.remote_address}.")
        while True:
            try:
                await self._handle_next_event()
            except curio.CancelledError:
                break
        logger.debug(f"Stopped handling events from {self.remote_address}.")
示例#11
0
    def push_update(self, update: GameStateUpdate) -> None:
        """Push a new state update to the update cache.

        This method will usually be called by whatever is progressing the game state,
        usually a #GameStateMachine.

        """
        self._game_state_update_cache.append(update)
        if len(self._game_state_update_cache) > self._update_cache_size:
            del self._game_state_update_cache[0]
        if update > self._game_state:
            logger.debug((
                f"Updating game state in state store from time order {self._game_state.time_order} "
                f"to {update.time_order}."))
            self._game_state += update
示例#12
0
 def _create_next_package(self):
     """Override #Connection._create_next_package to include game state updates."""
     update_cache = self.game_state_store.get_update_cache()
     # Respond by sending the sum of all updates since the client's time-order point.
     # Or the whole game state if the client doesn't have it yet.
     if self.last_client_time_order == 0:
         logger.debug(
             f"Sending full game state to client {self.remote_address}.")
         game_state = self.game_state_store.get_game_state()
         update = GameStateUpdate(**game_state.__dict__)
     else:
         update_base = GameStateUpdate(self.last_client_time_order)
         update = sum((upd for upd in update_cache if upd > update_base),
                      update_base)
         logger.debug((
             f"Sending update from time order {self.last_client_time_order} "
             f"to {update.time_order} to client {self.remote_address}."))
     return ServerPackage(
         Header(self.local_sequence, self.remote_sequence,
                self.ack_bitfield), update)
示例#13
0
 def __init__(self, remote_address: tuple, event_handler, event_wire=None):
     logger.debug(
         f"Creating connection instance for remote address {remote_address}."
     )
     self.remote_address = remote_address
     self.event_handler = event_handler
     self.event_wire = event_wire
     self.local_sequence = Sqn(0)
     self.remote_sequence = Sqn(0)
     self.ack_bitfield = "0" * 32
     self.latency = 0.0
     self.status = ConnectionStatus.get("Disconnected")
     self.quality = "good"  # this is used for congestion avoidance
     self._package_interval = self._package_intervals["good"]
     self._outgoing_event_queue = curio.UniversalQueue()
     self._incoming_event_queue = curio.UniversalQueue()
     self._pending_acks: dict = {}
     self._event_callback_sequence = Sqn(0)
     self._events_with_callbacks: dict = {}
     self._event_callbacks: dict = {}
     self._last_recv = time.time()
示例#14
0
    async def handle(self, event: Event, **kwargs):
        """Asynchronously invoke the appropriate handler function.

        This method is a coroutine and must be `await`ed.

        # Arguments
        event (Event): the event to be handled

        keyword arguments to be passed to the handler function (in addition to those already attached to the event)

        """
        logger.debug((
            f"Handling {event.type} event with args={event.handler_args} and kwargs={event.handler_kwargs}."
        ))
        if iscoroutinefunction(self._event_handlers[event.type]):
            return await self._event_handlers[event.type
                                              ](*event.handler_args,
                                                **dict(event.handler_kwargs,
                                                       **kwargs))
        return self._event_handlers[event.type](*event.handler_args,
                                                **dict(event.handler_kwargs,
                                                       **kwargs))
示例#15
0
 def _throttling_state_machine(self, t: int, state: dict):
     """Calculate a new state for congestion avoidance."""
     if self.quality == "good":
         if self.latency > self._latency_threshold:  # switch to bad mode
             logger.warning((
                 f"Throttling down connection to {self.remote_address} because "
                 f"latency ({self.latency}) is above latency threshold ({self._latency_threshold})."
             ))
             self.quality = "bad"
             self._package_interval = self._package_intervals["bad"]
             logger.debug(
                 f"new package interval: {self._package_interval} seconds.")
             # if good conditions didn't last at least the throttle time, increase it
             if t - state["last_quality_change"] < state["throttle_time"]:
                 state["throttle_time"] = min([
                     state["throttle_time"] * 2.0, self._max_throttle_time
                 ])
             state["last_quality_change"] = t
         # if good conditions lasted throttle time since last milestone
         elif t - state["last_good_quality_milestone"] > state[
                 "throttle_time"]:
             if self._package_interval > self._package_intervals["good"]:
                 logger.info((
                     f"Throttling up connection to {self.remote_address} because latency ({self.latency}) "
                     f"has been below latency threshold ({self._latency_threshold}) "
                     f"for {state['throttle_time']} seconds."))
                 self._package_interval = self._package_intervals["good"]
                 logger.debug(
                     f"new package interval: {self._package_interval} seconds."
                 )
             state["throttle_time"] = max(
                 [state["throttle_time"] / 2.0, self._min_throttle_time])
             state["last_good_quality_milestone"] = t
     else:  # self.quality == 'bad'
         if self.latency < self._latency_threshold:  # switch to good mode
             self.quality = "good"
             state["last_quality_change"] = t
             state["last_good_quality_milestone"] = t
示例#16
0
    async def _client_recv_loop(self, sock):
        """Continously handle packages received from the server.

        This coroutine, once spawned, will keep receiving packages from the server until it is explicitly
        cancelled.

        # Arguments
        sock (curio.io.Socket): socket with which to receive server packages

        """
        while self.local_sequence == 0:
            await curio.sleep(0)
        logger.debug(
            f"Starting to listen to packages from server at {self.remote_address}."
        )
        while True:
            try:
                data = await sock.recv(ServerPackage._max_size)  # pylint: disable=protected-access
                package = ServerPackage.from_datagram(data)
                await self._recv(package)
            except curio.CancelledError:
                break
        logger.debug(f"Stopped receiving packages from {self.remote_address}.")
示例#17
0
    async def _congestion_avoidance_monitor(self):
        """Continously monitor connection quality and throttle if needed.

        This coroutine will keep adjusting `self.quality` and throttling the rate at which packages are sent
        until it is explicitly cancelled.

        """
        state = {
            "throttle_time": self._min_throttle_time,
            "last_quality_change": time.time(),
            "last_good_quality_milestone": time.time(),
        }
        logger.debug(
            f"Starting congestion avoidance for connection to {self.remote_address}."
        )
        while True:
            try:
                self._throttling_state_machine(time.time(), state)
                await curio.sleep(self._min_throttle_time / 2.0)
            except curio.CancelledError:
                break
        logger.debug(
            f"Stopped congestion avoidance for connection to {self.remote_address}."
        )
示例#18
0
    async def _recv(self, package):
        """Handle a received package.

        Update `self.remote_sequence` and `self.ack_bitfield` based on `package`, resolve package loss
        and put the received events in the incoming event queue.

        # Raises
        DuplicateSequenceError: if a package with the same sequence has already been received

        """
        self._last_recv = time.time()
        if self.status != ConnectionStatus.get("Connected"):
            self._set_status("Connected")
        sequence, ack, ack_bitfield = package.header.destructure()
        logger.debug(
            f"Received package with sequence number {sequence} from {self.remote_address}."
        )
        self._update_remote_info(sequence)
        # resolve pending acks for sent packages (NEEDS REFACTORING)
        for pending_sequence in list(self._pending_acks):
            sequence_diff = ack - pending_sequence
            if sequence_diff == 0 or (0 < sequence_diff < 32 and
                                      ack_bitfield[sequence_diff - 1] == "1"):
                await self._handle_ack(pending_sequence)
            elif (time.time() - self._pending_acks[pending_sequence] >
                  Package._timeout  # pylint: disable=protected-access
                  ):
                await self._handle_timeout(pending_sequence)
        for event in package.events:
            await self._incoming_event_queue.put(event)
            logger.debug(
                f"Received event of type {event.type} from {self.remote_address}."
            )
            if self.event_wire is not None:
                logger.debug("Pushing event to event wire.")
                await self.event_wire._push_event(event)  # pylint: disable=protected-access
示例#19
0
 def __init__(self):
     logger.debug("Creating Client instance.")
     self.connection = None
     self._universal_event_handler = UniversalEventHandler()
示例#20
0
 async def _push_event(self, event: Event) -> None:  # pylint: disable=function-redefined
     logger.debug(
         f"State machine receiving event of type {event.type} via event wire."
     )
     await self._event_queue.put(event)