|
2 | 2 | import os |
3 | 3 | import random |
4 | 4 | import subprocess |
| 5 | +import threading |
5 | 6 | import time |
6 | 7 | from abc import ABC, abstractmethod |
7 | 8 | from pathlib import Path |
@@ -75,6 +76,10 @@ class CodeArena(ABC): |
75 | 76 | default_args: dict = {} |
76 | 77 | submission: str |
77 | 78 |
|
| 79 | + # Serializes image builds across concurrent pairs (e.g. `ladder make --workers N`), so |
| 80 | + # worker threads don't all race `docker build` on a cold start and fail with "already exists". |
| 81 | + _build_lock = threading.Lock() |
| 82 | + |
78 | 83 | def __init__(self, config: dict, *, tournament_id: str, local_output_dir: Path, keep_containers: bool = False): |
79 | 84 | """The CodeArena class is responsible for running games, i.e., taking a list of code |
80 | 85 | from different agents/players and running them against each other. |
@@ -127,36 +132,38 @@ def build_image(self): |
127 | 132 | if is_running_in_aws_batch(): |
128 | 133 | pull_game_container_aws_ecr(game_name=self.name, image_name=self.image_name, logger=self.logger) |
129 | 134 |
|
130 | | - # Check if container exists using subprocess |
131 | | - self.logger.debug(f"Checking if container {self.image_name} exists") |
132 | | - result = subprocess.run( |
133 | | - f"docker images -q {self.image_name}", |
134 | | - shell=True, |
135 | | - capture_output=True, |
136 | | - text=True, |
137 | | - ) |
138 | | - if result.stdout.strip(): |
139 | | - self.logger.debug(f"Container {self.image_name} exists") |
140 | | - return |
| 135 | + # Hold the lock across check-and-build so concurrent pairs don't race: the first thread |
| 136 | + # builds while the rest wait, then find the image already present and skip. |
| 137 | + with CodeArena._build_lock: |
| 138 | + self.logger.debug(f"Checking if container {self.image_name} exists") |
| 139 | + result = subprocess.run( |
| 140 | + f"docker images -q {self.image_name}", |
| 141 | + shell=True, |
| 142 | + capture_output=True, |
| 143 | + text=True, |
| 144 | + ) |
| 145 | + if result.stdout.strip(): |
| 146 | + self.logger.debug(f"Container {self.image_name} exists") |
| 147 | + return |
141 | 148 |
|
142 | | - self.logger.info( |
143 | | - f"Building Docker image {self.image_name}. This may take 1-5 minutes and only work on Linux for some games." |
144 | | - ) |
| 149 | + self.logger.info( |
| 150 | + f"Building Docker image {self.image_name}. This may take 1-5 minutes and only work on Linux for some games." |
| 151 | + ) |
145 | 152 |
|
146 | | - # NOTE: Assuming Dockerfile is declared in same directory as the arena. |
147 | | - arena_file = Path(inspect.getfile(self.__class__)) |
148 | | - folder_path = arena_file.parent |
149 | | - result = subprocess.run( |
150 | | - f"docker build --no-cache -t {self.image_name} -f {folder_path}/{self.name}.Dockerfile .", |
151 | | - shell=True, |
152 | | - capture_output=True, |
153 | | - text=True, |
154 | | - ) |
155 | | - if result.returncode == 0: |
156 | | - self.logger.info(f"✅ Built Docker image {self.image_name}") |
157 | | - else: |
158 | | - self.logger.error(f"❌ Failed to build Docker image: {result.stderr}\n{result.stdout}{result.stderr}") |
159 | | - raise RuntimeError(f"Failed to build Docker image: {result.stderr}") |
| 153 | + # NOTE: Assuming Dockerfile is declared in same directory as the arena. |
| 154 | + arena_file = Path(inspect.getfile(self.__class__)) |
| 155 | + folder_path = arena_file.parent |
| 156 | + result = subprocess.run( |
| 157 | + f"docker build --no-cache -t {self.image_name} -f {folder_path}/{self.name}.Dockerfile .", |
| 158 | + shell=True, |
| 159 | + capture_output=True, |
| 160 | + text=True, |
| 161 | + ) |
| 162 | + if result.returncode == 0: |
| 163 | + self.logger.info(f"✅ Built Docker image {self.image_name}") |
| 164 | + else: |
| 165 | + self.logger.error(f"❌ Failed to build Docker image: {result.stderr}\n{result.stdout}{result.stderr}") |
| 166 | + raise RuntimeError(f"Failed to build Docker image: {result.stderr}") |
160 | 167 |
|
161 | 168 | def copy_logs_from_env(self, round_num: int) -> None: |
162 | 169 | """Copy logs from the game's environment to the local machine.""" |
|
0 commit comments