Skip to content

Commit 7106163

Browse files
Weiwei YangEthanlm
authored andcommitted
Handle potential NPE in cleaner job verdicts (#1932)
* Handle potential NPE in cleaner job verdicts * fix format * fix logging and add TODO * Update axlearn/cloud/common/cleaner.py Co-authored-by: Ethan (Meng) Li <ethanli@apple.com> * Fix comments --------- Co-authored-by: Ethan (Meng) Li <ethanli@apple.com>
1 parent 8fc2c2a commit 7106163

2 files changed

Lines changed: 106 additions & 3 deletions

File tree

axlearn/cloud/common/cleaner.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Copyright © 2023 Apple Inc.
22

33
"""Utilities to clean resources."""
4-
4+
import logging
55
from collections.abc import Sequence
66
from enum import Enum
77

@@ -89,6 +89,12 @@ def sweep(self, jobs: dict[str, JobSpec]) -> Sequence[str]:
8989
schedule_result = scheduler.schedule(
9090
dict(my_job=job_spec.metadata),
9191
)
92-
if schedule_result.job_verdicts["my_job"].over_limits:
92+
job_verdict = schedule_result.job_verdicts.get("my_job")
93+
# In certain corner cases, schedule_result may not contain the job being passed in
94+
# TODO(yangwwei): revisit and fix it in a proper way
95+
if job_verdict is None:
96+
logging.warning("job %s does not exist in the verdicts, skipping", job_name)
97+
continue
98+
if job_verdict.over_limits:
9399
result.append(job_name)
94100
return result

axlearn/cloud/common/cleaner_test.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import datetime
66
from collections.abc import Sequence
7+
from unittest import mock
78

89
from absl.testing import parameterized
910

@@ -15,7 +16,7 @@
1516
UnschedulableCleaner,
1617
)
1718
from axlearn.cloud.common.quota import QuotaInfo
18-
from axlearn.cloud.common.scheduler import JobMetadata, JobScheduler
19+
from axlearn.cloud.common.scheduler import BaseScheduler, JobMetadata, JobScheduler, JobVerdict
1920
from axlearn.cloud.common.types import JobSpec
2021
from axlearn.common.config import REQUIRED, Required, config_class, config_for_function
2122

@@ -116,3 +117,99 @@ def mock_jobs(*, prefix: str, resource_count: int):
116117
cleaner = cfg.instantiate()
117118
result = cleaner.sweep(jobs)
118119
self.assertSequenceEqual(result, list(unschedulable_jobs.keys()))
120+
121+
def test_sweep_missing_job_in_verdicts(self):
122+
"""Test that sweep handles the case when job is not in job_verdicts."""
123+
# Create a mock schedule result without the job being scheduled
124+
mock_schedule_result = BaseScheduler.ScheduleResults(
125+
project_limits={},
126+
project_usages={},
127+
job_verdicts={}, # empty job verdicts
128+
unused_limits=None,
129+
)
130+
131+
# Create a simple job
132+
job = new_jobspec(
133+
name="test_job",
134+
command="echo hello",
135+
metadata=JobMetadata(
136+
user_id="user_1",
137+
project_id="default",
138+
creation_time=datetime.datetime(1900, 1, 1, 0, 0, 0),
139+
resources={"aws_4:p5.48xlarge": 1},
140+
),
141+
)
142+
jobs = {"test_job": job}
143+
144+
# Create cleaner
145+
quota = QuotaInfo(
146+
total_resources=[{"aws_4:p5.48xlarge": 10}],
147+
project_resources={},
148+
project_membership={},
149+
)
150+
cfg = UnschedulableCleaner.default_config().set(
151+
scheduler=JobScheduler.default_config().set(
152+
quota=config_for_function(lambda: lambda: quota)
153+
)
154+
)
155+
cleaner = cfg.instantiate()
156+
157+
# Patch the scheduler.schedule method to return our mock result
158+
# and verify the logging
159+
with self.assertLogs(level="INFO") as log_context:
160+
with mock.patch.object(JobScheduler, "schedule", return_value=mock_schedule_result):
161+
result = cleaner.sweep(jobs)
162+
163+
# Should return empty list since "my_job" is not in job_verdicts
164+
self.assertSequenceEqual(result, [])
165+
166+
# Verify the log message was emitted
167+
self.assertTrue(
168+
any(
169+
"job test_job does not exist in the verdicts, skipping" in log
170+
for log in log_context.output
171+
)
172+
)
173+
174+
def test_sweep_job_with_over_limits(self):
175+
"""Test that sweep correctly identifies jobs with over_limits."""
176+
# Create a mock schedule result with a verdict that has over_limits
177+
mock_schedule_result = BaseScheduler.ScheduleResults(
178+
project_limits={},
179+
project_usages={},
180+
job_verdicts={"my_job": JobVerdict(over_limits={"aws_4:p5.48xlarge"})},
181+
unused_limits=None,
182+
)
183+
184+
# Create a simple job
185+
job = new_jobspec(
186+
name="test_job",
187+
command="echo hello",
188+
metadata=JobMetadata(
189+
user_id="user_1",
190+
project_id="default",
191+
creation_time=datetime.datetime(1900, 1, 1, 0, 0, 0),
192+
resources={"aws_4:p5.48xlarge": 100},
193+
),
194+
)
195+
jobs = {"test_job": job}
196+
197+
# Create cleaner
198+
quota = QuotaInfo(
199+
total_resources=[{"aws_4:p5.48xlarge": 10}],
200+
project_resources={},
201+
project_membership={},
202+
)
203+
cfg = UnschedulableCleaner.default_config().set(
204+
scheduler=JobScheduler.default_config().set(
205+
quota=config_for_function(lambda: lambda: quota)
206+
)
207+
)
208+
cleaner = cfg.instantiate()
209+
210+
# Patch the scheduler.schedule method to return our mock result
211+
with mock.patch.object(JobScheduler, "schedule", return_value=mock_schedule_result):
212+
result = cleaner.sweep(jobs)
213+
214+
# Should return the job since it has over_limits
215+
self.assertSequenceEqual(result, ["test_job"])

0 commit comments

Comments
 (0)