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()
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()
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}." )
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
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
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
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}."))
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)]
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)
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}.")
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
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)
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()
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))
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
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}.")
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}." )
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
def __init__(self): logger.debug("Creating Client instance.") self.connection = None self._universal_event_handler = UniversalEventHandler()
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)