Skip to content

Commit 729ce80

Browse files
committed
Add lock for image construction
1 parent 0ceade9 commit 729ce80

1 file changed

Lines changed: 35 additions & 28 deletions

File tree

codeclash/arenas/arena.py

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import random
44
import subprocess
5+
import threading
56
import time
67
from abc import ABC, abstractmethod
78
from pathlib import Path
@@ -75,6 +76,10 @@ class CodeArena(ABC):
7576
default_args: dict = {}
7677
submission: str
7778

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+
7883
def __init__(self, config: dict, *, tournament_id: str, local_output_dir: Path, keep_containers: bool = False):
7984
"""The CodeArena class is responsible for running games, i.e., taking a list of code
8085
from different agents/players and running them against each other.
@@ -127,36 +132,38 @@ def build_image(self):
127132
if is_running_in_aws_batch():
128133
pull_game_container_aws_ecr(game_name=self.name, image_name=self.image_name, logger=self.logger)
129134

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
141148

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+
)
145152

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}")
160167

161168
def copy_logs_from_env(self, round_num: int) -> None:
162169
"""Copy logs from the game's environment to the local machine."""

0 commit comments

Comments
 (0)