Skip to content

Commit 8be3a0e

Browse files
committed
Feat(aws): Start batch monitor after jobs already submitted
1 parent 7a4570a commit 8be3a0e

4 files changed

Lines changed: 106 additions & 25 deletions

File tree

aws/run_job.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ def get_latest_job_definition_arn(self) -> str:
6161
logger.debug(f"Latest job definition:\n{json.dumps(latest_job_def, indent=2, default=str)}")
6262
return latest_job_def["jobDefinitionArn"]
6363

64-
def submit_job(self, command: list[str], job_name: str | None = None) -> str:
65-
"""Submit a job to AWS Batch. Returns jobs ID."""
64+
def submit_job(self, command: list[str], job_name: str | None = None) -> tuple[str, str]:
65+
"""Submit a job to AWS Batch. Returns (job_id, job_name)."""
6666
if job_name is None:
6767
human_name = command[0]
6868
# If have config take that instead
@@ -97,7 +97,7 @@ def submit_job(self, command: list[str], job_name: str | None = None) -> str:
9797
logger.info(f"Full Command (passed to container entrypoint/bootstrap.sh): {shlex.join(full_command)}")
9898
logger.info(f"To retrieve logs later, run: python get_job_log.py {job_id}")
9999

100-
return job_id
100+
return job_id, job_name
101101

102102
def get_job_status(self, job_id: str) -> dict[str, Any]:
103103
"""Get the current status of a job."""
@@ -186,7 +186,7 @@ def main():
186186
)
187187

188188
# Submit the job
189-
job_id = launcher.submit_job(command, args.job_name)
189+
job_id, job_name = launcher.submit_job(command, args.job_name)
190190

191191
# Wait for completion if requested
192192
if args.wait or args.show_logs:

aws/utils/job_monitor.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def create_status_table(self, jobs_info: dict[str, dict]) -> Table:
5555
table.add_column("Status")
5656
table.add_column("Runtime", justify="right", style="magenta")
5757
table.add_column("Job ID", style="yellow")
58+
table.add_column("Job Name", style="blue")
5859
table.add_column("Config", style="green")
5960

6061
for job_id, info in jobs_info.items():
@@ -65,43 +66,61 @@ def create_status_table(self, jobs_info: dict[str, dict]) -> Table:
6566
table.add_row(
6667
status_colored,
6768
info["runtime"],
68-
job_id,
69+
job_id[:13] + "...",
70+
info["job_name"],
6971
info["config"],
7072
)
7173

7274
return table
7375

74-
def monitor(self, job_id_to_config: dict[str, str]) -> None:
75-
"""Monitor submitted jobs and display status in real-time"""
76+
def monitor(self, job_info_dict: dict[str, dict[str, str]]) -> None:
77+
"""Monitor submitted jobs and display status in real-time.
78+
79+
Args:
80+
job_info_dict: Dict mapping job_id -> {job_name, config}
81+
"""
7682
jobs_info: dict[str, dict] = {}
77-
start_times: dict[str, float] = {job_id: time.time() for job_id in job_id_to_config}
83+
start_times: dict[str, float] = {job_id: time.time() for job_id in job_info_dict}
84+
completion_times: dict[str, float] = {}
7885

7986
self.console.print("\n[bold green]Note:[/] You can ^C this script without your jobs dying\n")
8087

8188
try:
8289
with Live(self.create_status_table({}), refresh_per_second=1, console=self.console) as live:
8390
while True:
8491
all_done = True
85-
for job_id, config in job_id_to_config.items():
92+
for job_id, info in job_info_dict.items():
8693
try:
8794
job_status = self.get_job_status(job_id)
8895
status = job_status["status"]
89-
elapsed = time.time() - start_times[job_id]
96+
97+
# Freeze runtime when job reaches terminal state
98+
if status in ["SUCCEEDED", "FAILED"] and job_id not in completion_times:
99+
completion_times[job_id] = time.time()
100+
101+
if job_id in completion_times:
102+
elapsed = completion_times[job_id] - start_times[job_id]
103+
else:
104+
elapsed = time.time() - start_times[job_id]
90105

91106
jobs_info[job_id] = {
92107
"status": status,
93108
"runtime": self.format_duration(elapsed),
94-
"config": config,
109+
"job_name": info["job_name"],
110+
"config": info["config"],
95111
}
96112

97113
if status not in ["SUCCEEDED", "FAILED"]:
98114
all_done = False
99115
except Exception as e:
100116
logger.error(f"Error getting status for {job_id}: {e}", exc_info=True)
117+
if job_id not in completion_times:
118+
completion_times[job_id] = time.time()
101119
jobs_info[job_id] = {
102120
"status": "ERROR",
103-
"runtime": self.format_duration(time.time() - start_times[job_id]),
104-
"config": config,
121+
"runtime": self.format_duration(completion_times[job_id] - start_times[job_id]),
122+
"job_name": info["job_name"],
123+
"config": info["config"],
105124
}
106125

107126
live.update(self.create_status_table(jobs_info))

batch_monitor.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import json
5+
from pathlib import Path
6+
7+
from aws.utils.job_monitor import JobMonitor
8+
from codeclash.utils.log import get_logger
9+
10+
logger = get_logger("batch_monitor", emoji="👁️")
11+
12+
13+
def load_job_files(job_files: list[Path]) -> dict[str, dict[str, str]]:
14+
"""Load job information from multiple JSON files.
15+
16+
Args:
17+
job_files: List of paths to batch_submit_{timestamp}.json files
18+
19+
Returns:
20+
Combined dict mapping job_id -> {job_name, config}
21+
"""
22+
all_jobs: dict[str, dict[str, str]] = {}
23+
24+
for job_file in job_files:
25+
logger.info(f"Loading jobs from {job_file}")
26+
jobs = json.loads(job_file.read_text())
27+
28+
# Handle both old format (job_id -> config) and new format (job_id -> {job_name, config})
29+
for job_id, value in jobs.items():
30+
if isinstance(value, dict):
31+
all_jobs[job_id] = value
32+
else:
33+
# Old format, convert to new format
34+
all_jobs[job_id] = {"job_name": "N/A", "config": value}
35+
36+
logger.info(f"Loaded {len(all_jobs)} jobs total")
37+
return all_jobs
38+
39+
40+
def main() -> None:
41+
parser = argparse.ArgumentParser(description="Monitor AWS Batch jobs from multiple batch_submit JSON files")
42+
parser.add_argument(
43+
"job_files",
44+
type=Path,
45+
nargs="+",
46+
help="One or more batch_submit_{timestamp}.json files to monitor",
47+
)
48+
parser.add_argument(
49+
"--region",
50+
default="us-east-1",
51+
help="AWS region (default: us-east-1)",
52+
)
53+
args = parser.parse_args()
54+
55+
all_jobs = load_job_files(args.job_files)
56+
57+
monitor = JobMonitor(region=args.region)
58+
monitor.monitor(all_jobs)
59+
60+
61+
if __name__ == "__main__":
62+
main()

batch_submit.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,32 @@ def __init__(
2929
)
3030
self.monitor = JobMonitor(region=region)
3131

32-
def submit_configs(self, configs: list[str]) -> dict[str, str]:
33-
"""Submit jobs for a list of config files. Returns job_id -> config mapping."""
32+
def submit_configs(self, configs: list[str]) -> dict[str, dict[str, str]]:
33+
"""Submit jobs for a list of config files. Returns job_id -> {job_name, config} mapping."""
3434
logger.info(f"Launching {len(configs)} configs")
35-
job_id_to_config: dict[str, str] = {}
35+
job_info: dict[str, dict[str, str]] = {}
3636

3737
for config in configs:
3838
config_path = self.config_dir / config
3939
command = ["aws/docker_and_sync.sh", "python", "main.py", str(config_path)]
40-
job_id = self.launcher.submit_job(command)
41-
job_id_to_config[job_id] = config
40+
job_id, job_name = self.launcher.submit_job(command)
41+
job_info[job_id] = {"job_name": job_name, "config": config}
4242

43-
return job_id_to_config
43+
return job_info
4444

45-
def save_job_ids(self, job_id_to_config: dict[str, str]) -> Path:
46-
"""Save job IDs to a timestamped JSON file."""
45+
def save_job_ids(self, job_info: dict[str, dict[str, str]]) -> Path:
46+
"""Save job IDs and names to a timestamped JSON file."""
4747
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
4848
output_file = Path(f"batch_submit_{timestamp}.json")
49-
output_file.write_text(json.dumps(job_id_to_config, indent=2))
49+
output_file.write_text(json.dumps(job_info, indent=2))
5050
logger.info(f"Job IDs saved to {output_file}")
5151
return output_file
5252

5353
def run(self, configs: list[str]) -> None:
5454
"""Submit jobs, save IDs, and monitor."""
55-
job_id_to_config = self.submit_configs(configs)
56-
self.save_job_ids(job_id_to_config)
57-
self.monitor.monitor(job_id_to_config)
55+
job_info = self.submit_configs(configs)
56+
self.save_job_ids(job_info)
57+
self.monitor.monitor(job_info)
5858

5959

6060
def main() -> None:

0 commit comments

Comments
 (0)