Skip to content

Commit e10fb10

Browse files
committed
Enh(aws): Incremental syncing to aws
1 parent 950bbac commit e10fb10

6 files changed

Lines changed: 69 additions & 7 deletions

File tree

aws/docker_and_sync.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ docker run hello-world
6363
# Pull images from ECR so we don't have to build them
6464
export AWS_DOCKER_REGISTRY="039984708918.dkr.ecr.us-east-1.amazonaws.com"
6565
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $AWS_DOCKER_REGISTRY
66+
export AWS_S3_BUCKET="codeclash"
67+
export AWS_S3_PREFIX="logs"
6668

6769
# Create logs directory
6870
mkdir -p logs

codeclash/analysis/matrix.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from codeclash.constants import DIR_WORK
1212
from codeclash.games import get_game
1313
from codeclash.tournaments.utils.git_utils import filter_git_diff
14+
from codeclash.utils.atomic_write import atomic_write
1415
from codeclash.utils.log import add_file_handler, get_logger
1516

1617
# todo: add visualization code
@@ -90,11 +91,7 @@ def matrices(self) -> dict:
9091
def _save(self):
9192
"""Save metadata to matrix.json."""
9293
with self._save_lock:
93-
# let's make this high stakes write atomic, because else if you run out of disk space,
94-
# you'll lose all progress
95-
tmp_file = self.output_file.with_suffix(".tmp")
96-
tmp_file.write_text(json.dumps(self._metadata, indent=2))
97-
tmp_file.rename(self.output_file)
94+
atomic_write(self.output_file, json.dumps(self._metadata, indent=2))
9895

9996
def _load_existing_progress(self):
10097
"""Load existing progress from matrix.json if it exists."""

codeclash/tournaments/pvp.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from codeclash.games import get_game
1616
from codeclash.games.game import CodeGame
1717
from codeclash.tournaments.tournament import AbstractTournament
18+
from codeclash.utils.atomic_write import atomic_write
19+
from codeclash.utils.aws import is_running_in_aws_batch, s3_log_sync
1820
from codeclash.utils.environment import copy_to_container
1921

2022

@@ -127,7 +129,11 @@ def run_agent(self, agent: Player, round_num: int) -> None:
127129

128130
def _save(self) -> None:
129131
self.local_output_dir.mkdir(parents=True, exist_ok=True)
130-
(self.local_output_dir / "metadata.json").write_text(json.dumps(self.get_metadata(), indent=2))
132+
metadata_file = self.local_output_dir / "metadata.json"
133+
atomic_write(metadata_file, json.dumps(self.get_metadata(), indent=2))
134+
self.logger.debug(f"Metadata saved to {metadata_file}")
135+
if is_running_in_aws_batch():
136+
s3_log_sync(self.local_output_dir, logger=self.logger)
131137

132138
def _compress_round_logs(self) -> None:
133139
self.logger.info("Compressing round logs, this might take a while...")

codeclash/tournaments/single_player.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from codeclash.games.game import CodeGame
1616
from codeclash.tournaments.tournament import AbstractTournament
1717
from codeclash.tournaments.utils.git_utils import filter_git_diff
18+
from codeclash.utils.atomic_write import atomic_write
19+
from codeclash.utils.aws import is_running_in_aws_batch, s3_log_sync
1820
from codeclash.utils.environment import copy_to_container
1921

2022

@@ -131,7 +133,9 @@ def set_mirror_state_to_round(self, round_num: int):
131133

132134
def _save(self) -> None:
133135
self.local_output_dir.mkdir(parents=True, exist_ok=True)
134-
(self.local_output_dir / "metadata.json").write_text(json.dumps(self.get_metadata(), indent=2))
136+
atomic_write(self.local_output_dir / "metadata.json", json.dumps(self.get_metadata(), indent=2))
137+
if is_running_in_aws_batch():
138+
s3_log_sync(self.local_output_dir, logger=self.logger)
135139

136140
def end(self):
137141
"""Clean up game resources."""

codeclash/utils/atomic_write.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from pathlib import Path
2+
3+
4+
def atomic_write(path: Path, text: str) -> None:
5+
"""Write text to file atomically to prevent corruption on interruption."""
6+
tmp_file = path.with_suffix(".tmp")
7+
tmp_file.write_text(text)
8+
tmp_file.rename(path)

codeclash/utils/aws.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import os
22
import subprocess
33
from logging import Logger
4+
from pathlib import Path
5+
6+
from codeclash.constants import DIR_LOGS
47

58

69
def is_running_in_aws_batch() -> bool:
@@ -56,3 +59,45 @@ def pull_game_container_aws_ecr(*, game_name: str, image_name: str, logger: Logg
5659
raise RuntimeError(f"Failed to pull and tag Docker image: {result.stderr}")
5760

5861
logger.info(f"✅ Pulled and tagged Docker image {image_name}")
62+
63+
64+
def s3_log_sync(local_output_dir: Path, *, logger: Logger) -> None:
65+
"""Sync local logs to S3 bucket.
66+
67+
Args:
68+
local_output_dir: Local directory containing logs to sync
69+
logger: Logger instance for debug output
70+
71+
Raises:
72+
AssertionError: If required environment variables are not set
73+
RuntimeError: If aws s3 sync fails
74+
"""
75+
aws_s3_bucket = os.getenv("AWS_S3_BUCKET")
76+
aws_s3_prefix = os.getenv("AWS_S3_PREFIX")
77+
78+
assert aws_s3_bucket is not None, "AWS_S3_BUCKET environment variable must be set"
79+
assert aws_s3_prefix is not None, "AWS_S3_PREFIX environment variable must be set"
80+
81+
# Construct S3 path: s3://bucket/prefix/logs/relative_path
82+
# where relative_path is local_output_dir relative to DIR_LOGS
83+
try:
84+
relative_path = local_output_dir.relative_to(DIR_LOGS)
85+
except ValueError:
86+
# If local_output_dir is not under DIR_LOGS, use the full path
87+
relative_path = local_output_dir
88+
89+
s3_path = f"s3://{aws_s3_bucket}/{aws_s3_prefix}/logs/{relative_path}"
90+
91+
logger.debug(f"Syncing {local_output_dir} to {s3_path}")
92+
93+
result = subprocess.run(
94+
["aws", "s3", "sync", str(local_output_dir), s3_path, "--exclude", "*/rounds/*"],
95+
capture_output=True,
96+
text=True,
97+
)
98+
99+
if result.returncode != 0:
100+
logger.critical(f"❌ Failed to sync logs to S3: {result.stderr}\n{result.stdout}")
101+
raise RuntimeError(f"Failed to sync logs to S3: {result.stderr}")
102+
103+
logger.info(f"✅ Successfully synced logs to {s3_path}")

0 commit comments

Comments
 (0)