-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Finished ThuaiJudger and ThuaiBuilder
- Loading branch information
1 parent
017c294
commit 975d5a3
Showing
3 changed files
with
269 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -170,3 +170,4 @@ cython_debug/ | |
|
||
|
||
.vscode/ | ||
record/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import docker.errors | ||
from base_docker_image_builder import BaseDockerImageBuilder | ||
from typing import Dict | ||
from pathlib import Path | ||
import docker | ||
import string, random | ||
|
||
BUILDER_NAME_LENGTH = 10 | ||
IMAGE_NAME_PREFIX = "THUAI-image" | ||
|
||
class ThuaiBuilder(BaseDockerImageBuilder): | ||
|
||
def __init__(self): | ||
self.client = docker.from_env() | ||
self.built_images = dict() | ||
|
||
# Generate a random string for the builder, to avoid name conflict | ||
chars = string.ascii_letters + string.digits | ||
self.name = ''.join(random.choice(chars) for _ in range(BUILDER_NAME_LENGTH)) | ||
|
||
# ID for image, to avoid name conflict | ||
self.image_id = 0 | ||
|
||
async def build(self, file_path: Path) -> str: | ||
# if not built yet... | ||
if file_path not in self.built_images.keys(): | ||
try: | ||
img_tag = self.get_image_name() | ||
image, log = self.client.images.build(path=file_path, | ||
tag=img_tag, | ||
rm=True | ||
) | ||
self.built_images[file_path] = img_tag | ||
|
||
except Exception as e: | ||
raise | ||
|
||
return self.built_images[file_path] | ||
|
||
async def clean(self) -> None: | ||
for image_tag in self.built_images.values(): | ||
self.client.images.remove(image=image_tag) | ||
self.built_images.clear() | ||
|
||
|
||
async def list(self) -> Dict[Path, str]: | ||
return self.built_images.copy() | ||
|
||
async def get_image_name(self) -> str: | ||
'''Get a no-duplicate name for images | ||
Name format: image-{builder name}-{id} | ||
Returns: | ||
The generated image name. | ||
''' | ||
name = IMAGE_NAME_PREFIX + self.name + '-' + str(self.image_id) | ||
self.image_id = self.image_id + 1 | ||
return name | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
"""Contains the judger class implemented for THUAI matches.""" | ||
|
||
from base_match_judger import BaseMatchJudger | ||
from match_result import MatchResult | ||
from typing import List, Dict | ||
from pathlib import Path | ||
import docker | ||
import random, string | ||
from docker.types import Mount | ||
import asyncio | ||
import json | ||
|
||
|
||
JUDGER_NAME_LENGTH = 10 | ||
AGENT_CONTAINER_NAME_PREFIX = "THUAI_agent_" | ||
SERVER_CONTAINER_NAME_PREFIX = "THUAI_server_" | ||
NETWORK_NAME_PREFIX = "THUAI_network_" | ||
|
||
JUDGE_TIMEOUT = 600 # In seconds | ||
|
||
class JudgeTimeoutException(Exception): | ||
def __init__(self): | ||
super().__init__() | ||
|
||
class ThuaiJudger(BaseMatchJudger): | ||
"""The Judger implemented for THUAI judgement work.""" | ||
|
||
def __init__(self): | ||
"""Initialize the judger | ||
""" | ||
self.client = docker.from_env() | ||
self.judges = dict() | ||
self.containers = [] | ||
|
||
# Generate a random string for the judger, to avoid name conflict | ||
chars = string.ascii_letters + string.digits | ||
self.name = ''.join(random.choice(chars) for _ in range(JUDGER_NAME_LENGTH)) | ||
|
||
# IDs for the resources, to avoid name conflict | ||
self.agent_id = 0 | ||
self.server_id = 0 | ||
self.network_id = 0 | ||
|
||
# Record resources held by each judge. | ||
self.judge_containers : Dict[str, List[str]] = dict() | ||
self.judge_networks : Dict[str, List[str]] = dict() | ||
|
||
async def judge( | ||
self, match_id: str, game_host_image_tag: str, agent_image_tags: List[str] | ||
) -> MatchResult: | ||
|
||
# If not judged before... | ||
if match_id not in self.judges.keys(): | ||
try: | ||
# Judge and memorize the result. | ||
|
||
# Initialize the judge | ||
self.judge_containers[match_id] = [] | ||
self.judge_networks[match_id] = [] | ||
|
||
# Decide name of server. | ||
server_name = self.get_name("server") | ||
|
||
token = 0 | ||
|
||
# Run agent containers, create networks | ||
for agent_image_tag in agent_image_tags: | ||
|
||
# Network | ||
network_name = self.get_name("network") | ||
self.client.networks.create(network_name) | ||
self.judge_networks[match_id].append(network_name) | ||
|
||
# Agent Container | ||
container_name = self.get_name("agent") | ||
self.client.containers.run(agent_image_tag, [ | ||
"--token", str(token), "--server", f"ws://{server_name}:14514" | ||
], network=network_name, detach=True, name=container_name) | ||
self.judge_containers[match_id].append(container_name) | ||
|
||
token = token + 1 | ||
|
||
# Run server container. | ||
record_folder = self.get_name("record", match_id) | ||
Path(record_folder).mkdir(parents=True) | ||
server_mount = Mount("/record", record_folder, type="bind") | ||
|
||
self.client.containers.run( | ||
game_host_image_tag, | ||
ports={"14514/tcp": 14514}, | ||
mounts=[server_mount], | ||
detach=True, | ||
name=server_name | ||
) | ||
|
||
for network in self.judge_networks[match_id]: | ||
self.client.networks.get(network).connect(server_name) | ||
|
||
task_server_run = asyncio.create_task(self.wait_container(server_name)) | ||
task_force_kill = asyncio.create_task(self.force_kill(match_id)) | ||
|
||
await task_server_run | ||
|
||
self.stop_judge(match_id) | ||
|
||
winner = self.get_winner(match_id) | ||
scores = [1.0 if i == winner else 0.0 for i in range(token)] | ||
|
||
self.judges[match_id] = MatchResult(match_id, scores) | ||
|
||
task_force_kill.cancel() | ||
|
||
except Exception: | ||
self.stop_judge(match_id) | ||
raise | ||
|
||
return self.judges[match_id] | ||
|
||
async def wait_container(self, container_name: str): | ||
"""Wait before a detached container finishes running. | ||
Args: | ||
container_name (str): Name of the container. | ||
""" | ||
self.client.containers.get(container_name).wait() | ||
|
||
async def list(self) -> Dict[str, MatchResult]: | ||
return self.judges.copy() | ||
|
||
async def force_kill(self, match_id, waiting: int=JUDGE_TIMEOUT) -> None: | ||
"""Force stop a match and throw an exception. | ||
Args: | ||
match_id (str): The ID of match | ||
waiting (int, optional): The time to wait. Defaults to JUDGE_TIMEOUT. | ||
Raises: | ||
JudgeTimeoutException: If judge is force stopped. | ||
""" | ||
await asyncio.sleep(waiting) | ||
self.stop_judge(match_id) | ||
raise JudgeTimeoutException | ||
|
||
def stop_judge(self, match_id: str) -> None: | ||
"""Stop a judge and release its resources | ||
If match doesn't exist or has already ended, do nothing. | ||
Args: | ||
match_id (str): The match to stop. | ||
""" | ||
|
||
if match_id in self.judge_containers.keys(): | ||
for container in self.judge_containers[match_id]: | ||
self.client.containers.get(container).kill() | ||
self.judge_containers.pop(match_id) | ||
|
||
if match_id in self.judge_networks.keys(): | ||
for network in self.judge_networks[match_id]: | ||
self.client.networks.get(network).remove() | ||
self.judge_networks.pop(match_id) | ||
|
||
def get_winner(self, match_id) -> int: | ||
"""Get the winner of a game | ||
Args: | ||
match_id (str): The ID of the match | ||
Returns: | ||
int: The player id | ||
""" | ||
with open(Path(self.get_name("record", match_id)) / "result.json", 'r') as f: | ||
return int(json.load(f)["winner"]) | ||
|
||
|
||
def get_name(self, type: str, match_id="") -> str: | ||
"""Generates a no-duplicate name for containers and networks. | ||
Name format: THUAI_{type}_{judger_name}_{id} (except for "record") | ||
Record format: $(pwd)/record/{judger_name}/{match_id} | ||
Record means the folder to store the result. | ||
Args: | ||
type (str): Should be "agent", "server", "record" or "network". | ||
Returns: | ||
The name. | ||
Raises: | ||
ValueError: If type is illegal. | ||
""" | ||
if type == "agent": | ||
name = AGENT_CONTAINER_NAME_PREFIX + self.name + '_' + str(self.agent_id) | ||
self.agent_id = self.agent_id + 1 | ||
elif type == "server": | ||
name = SERVER_CONTAINER_NAME_PREFIX + self.name + '_' + str(self.server_id) | ||
self.server_id = self.server_id + 1 | ||
elif type == "network": | ||
name = NETWORK_NAME_PREFIX + self.name + '_' + str(self.network_id) | ||
self.network_id = self.network_id + 1 | ||
elif type == "record": | ||
name = str(Path.cwd() / "record" / self.name / match_id) | ||
pass | ||
else: | ||
raise ValueError | ||
return name | ||
|