WIP: All bots connect and run

Signed-off-by: Tyler Marques <me@tylermarques.com>
This commit is contained in:
Tyler Marques 2025-07-30 12:46:34 -07:00
parent 298c5df74a
commit 69aa40be31
No known key found for this signature in database
GPG key ID: CB99EDCF41D3016F
2 changed files with 31 additions and 67 deletions

View file

@ -49,6 +49,7 @@ async def main():
"""Main entry point with comprehensive error handling."""
bots = {}
args = parse_arguments()
bot_tasks = set()
if not args.game_id:
# No game id, lets create a new game.
@ -67,11 +68,10 @@ async def main():
)
for power, bot in bots.items():
try:
await bot.run()
logger.info(f"Bot {bot.power} started")
finally:
await bot.cleanup()
task = asyncio.create_task(bot.run())
bot_tasks.add(task)
logger.info(f"Bot_{bot.power_name} started")
await asyncio.gather(*bot_tasks)
else:
bot = SingleBotPlayer(

View file

@ -105,12 +105,13 @@ class SingleBotPlayer:
def _signal_handler(self, signum, frame):
"""Handle shutdown signals gracefully."""
logger.info(f"Received signal {signum}, shutting down...")
self.running = False
if self.shutdown_requested:
asyncio.run(self.cleanup)
loop = asyncio.get_running_loop()
loop.create_task(self.cleanup())
if not self.running:
logger.info("Already shutting down, send signal again for immediate shutdown")
self.shutdown_requested = True
self.running = False
async def connect_and_initialize(self):
"""Connect to the server and initialize the bot."""
@ -581,94 +582,57 @@ class SingleBotPlayer:
# Main event loop
while self.running and not self.client.game.is_game_done:
try:
# Synchronize with server periodically with retry logic
await self.circuit_breaker._retry_with_backoff(self.client.game.synchronize)
# Synchronize with server periodically with retry logic
await self.circuit_breaker._retry_with_backoff(self.client.game.synchronize)
# Check if we need to submit orders
await self._check_if_orders_needed()
# Check if we need to submit orders
await self._check_if_orders_needed()
# Sleep for a bit before next iteration
await asyncio.sleep(5)
except (asyncio.CancelledError, KeyboardInterrupt):
logger.info("Bot operation cancelled, shutting down")
break
except (TimeoutError, asyncio.TimeoutError) as e:
logger.warning(f"Timeout in main loop: {e}")
# Continue loop but with a longer sleep
await asyncio.sleep(10)
# Sleep for a bit before next iteration
await asyncio.sleep(5)
if self.client.game.is_game_done:
logger.info("Game has finished")
else:
logger.info("Bot shutting down")
except GameIdException:
except GameIdException as e:
logger.error(f"Game with id {self.game_id} does not exist on the server. Exiting...")
except (asyncio.CancelledError, KeyboardInterrupt):
raise e
except (asyncio.CancelledError, KeyboardInterrupt) as e:
logger.info("Bot cancelled or interrupted")
raise e
finally:
await self.cleanup()
async def cleanup(self):
"""Clean up resources with timeout protection."""
# Prevent multiple cleanup calls
if hasattr(self, "_cleanup_done") and self._cleanup_done:
return
self._cleanup_done = True
logger.info("Starting cleanup process...")
cleanup_timeout = 15.0 # Maximum time to spend on cleanup
cleanup_timeout = 5.0 # Maximum time to spend on cleanup
try:
# Use asyncio.wait_for to prevent hanging during cleanup
await asyncio.wait_for(self._perform_cleanup(), timeout=cleanup_timeout)
logger.info("Cleanup completed successfully")
except asyncio.TimeoutError:
logger.warning(f"Cleanup timed out after {cleanup_timeout} seconds")
logger.warning("Cleanup timed out")
except Exception as e:
logger.error(f"Error during cleanup: {e}")
finally:
logger.info(f"Cleanup finished for bot_{self.power_name}")
async def _perform_cleanup(self):
"""Perform the actual cleanup operations."""
cleanup_tasks = []
# Game cleanup
if hasattr(self, "client") and self.client and hasattr(self.client, "game") and self.client.game:
logger.debug("Cleaning up game connection...")
# Use asyncio.create_task to make game.leave() non-blocking
leave_task = asyncio.create_task(self._safe_game_leave())
cleanup_tasks.append(leave_task)
await self.client.game.leave()
# Client cleanup
if hasattr(self, "client") and self.client:
logger.debug("Cleaning up client connection...")
close_task = asyncio.create_task(self._safe_client_close())
cleanup_tasks.append(close_task)
# Wait for all cleanup tasks with individual timeouts
if cleanup_tasks:
done, pending = await asyncio.wait(
cleanup_tasks,
timeout=10.0, # 10 second timeout for all cleanup tasks
return_when=asyncio.ALL_COMPLETED,
)
# Cancel any pending tasks
for task in pending:
logger.warning(f"Cancelling pending cleanup task: {task}")
task.cancel()
try:
await task
except asyncio.CancelledError as e:
logger.warning(f"Task was cancelled, rasied {e}", exc_info=True)
async def _safe_game_leave(self):
"""Safely leave the game with timeout."""
try:
# Some diplomacy client implementations have async leave, others are sync
await self.client.game.leave()
logger.debug("Successfully left game")
except asyncio.TimeoutError:
logger.warning("Game leave operation timed out")
async def _safe_client_close(self):
"""Safely close the client with timeout."""
try:
await asyncio.wait_for(self.client.close(), timeout=5.0)
logger.debug("Successfully closed client")
except asyncio.TimeoutError:
logger.warning("Client close operation timed out")
await self.client.close()