|
18 | 18 | It tests KubernetesBackend's behavior across job listing, resource creation etc. |
19 | 19 | """ |
20 | 20 |
|
| 21 | +import copy |
21 | 22 | from dataclasses import asdict |
22 | 23 | import datetime |
23 | 24 | import logging |
|
71 | 72 | FAIL_LOGS = "fail_logs" |
72 | 73 | LIST_RUNTIMES = "list_runtimes" |
73 | 74 | BASIC_TRAIN_JOB_NAME = "basic-job" |
| 75 | +JOB_WITH_POD_RESTARTS = "job-with-pod-restarts" |
74 | 76 | TRAIN_JOBS = "trainjobs" |
75 | 77 | TRAIN_JOB_WITH_BUILT_IN_TRAINER = "train-job-with-built-in-trainer" |
76 | 78 | TRAIN_JOB_WITH_CUSTOM_TRAINER = "train-job-with-custom-trainer" |
@@ -128,8 +130,13 @@ def conditional_error_handler(*args, **kwargs): |
128 | 130 |
|
129 | 131 |
|
130 | 132 | def list_namespaced_pod_response(*args, **kwargs): |
131 | | - """Return mock pod list response.""" |
132 | | - pod_list = get_mock_pod_list() |
| 133 | + """Return a mock pod list response for the requested TrainJob.""" |
| 134 | + label_selector = kwargs.get("label_selector", "") |
| 135 | + pod_list = ( |
| 136 | + get_mock_pod_list_with_restarts() |
| 137 | + if JOB_WITH_POD_RESTARTS in label_selector |
| 138 | + else get_mock_pod_list() |
| 139 | + ) |
133 | 140 | mock_thread = Mock() |
134 | 141 | mock_thread.get.return_value = pod_list |
135 | 142 | return mock_thread |
@@ -210,6 +217,30 @@ def get_mock_pod_list(): |
210 | 217 | ) |
211 | 218 |
|
212 | 219 |
|
| 220 | +def get_mock_pod_list_with_restarts() -> models.IoK8sApiCoreV1PodList: |
| 221 | + """Create Pods where newer replacements share the same TrainJob component roles.""" |
| 222 | + old_timestamp = datetime.datetime(2025, 6, 1, 10, 0, 0) |
| 223 | + new_timestamp = datetime.datetime(2025, 6, 1, 11, 0, 0) |
| 224 | + old_pods = get_mock_pod_list().items |
| 225 | + node_1_pod = copy.deepcopy(old_pods[-1]) |
| 226 | + node_1_pod.metadata.name = "node-1-pod" |
| 227 | + node_1_pod.metadata.labels[constants.JOB_INDEX_LABEL] = "1" |
| 228 | + old_pods.append(node_1_pod) |
| 229 | + restarted_pods = [] |
| 230 | + |
| 231 | + for old_pod in old_pods: |
| 232 | + old_pod.metadata.creation_timestamp = old_timestamp |
| 233 | + old_pod.metadata.labels[constants.JOBSET_NAME_LABEL] = JOB_WITH_POD_RESTARTS |
| 234 | + |
| 235 | + restarted_pod = copy.deepcopy(old_pod) |
| 236 | + restarted_pod.metadata.name = f"{old_pod.metadata.name}-restarted" |
| 237 | + restarted_pod.metadata.creation_timestamp = new_timestamp |
| 238 | + restarted_pod.status.phase = constants.POD_PENDING |
| 239 | + restarted_pods.append(restarted_pod) |
| 240 | + |
| 241 | + return models.IoK8sApiCoreV1PodList(items=[*old_pods, *restarted_pods]) |
| 242 | + |
| 243 | + |
213 | 244 | def get_resource_requirements() -> models.IoK8sApiCoreV1ResourceRequirements: |
214 | 245 | """Create a mock ResourceRequirements object for testing.""" |
215 | 246 | return models.IoK8sApiCoreV1ResourceRequirements( |
@@ -726,6 +757,7 @@ def get_train_job_data_type( |
726 | 757 | num_nodes=2, |
727 | 758 | image="example.com/test-runtime", |
728 | 759 | ) |
| 760 | + |
729 | 761 | trainer.set_command(constants.TORCH_COMMAND) |
730 | 762 | return types.TrainJob( |
731 | 763 | name=train_job_name, |
@@ -763,6 +795,25 @@ def get_train_job_data_type( |
763 | 795 | ) |
764 | 796 |
|
765 | 797 |
|
| 798 | +def get_train_job_with_restarted_pods_data_type( |
| 799 | + runtime_name: str, |
| 800 | + train_job_name: str, |
| 801 | +) -> types.TrainJob: |
| 802 | + """Create the expected TrainJob after newer replacement Pods are selected.""" |
| 803 | + train_job = get_train_job_data_type(runtime_name, train_job_name) |
| 804 | + |
| 805 | + for step in train_job.steps: |
| 806 | + step.pod_name = f"{step.pod_name}-restarted" |
| 807 | + step.status = constants.POD_PENDING |
| 808 | + |
| 809 | + node_1_step = copy.deepcopy(train_job.steps[-1]) |
| 810 | + node_1_step.name = "node-1" |
| 811 | + node_1_step.pod_name = "node-1-pod-restarted" |
| 812 | + train_job.steps.append(node_1_step) |
| 813 | + |
| 814 | + return train_job |
| 815 | + |
| 816 | + |
766 | 817 | def _run_verify_backend_with_core_api(core_api: Mock) -> tuple[list[str], int]: |
767 | 818 | """Helper to run verify_backend and capture warning logs.""" |
768 | 819 |
|
@@ -1447,6 +1498,15 @@ def test_train(kubernetes_backend, test_case): |
1447 | 1498 | train_job_name=BASIC_TRAIN_JOB_NAME, |
1448 | 1499 | ), |
1449 | 1500 | ), |
| 1501 | + TestCase( |
| 1502 | + name="returns only the newest Pod for each TrainJob component", |
| 1503 | + expected_status=SUCCESS, |
| 1504 | + config={"name": JOB_WITH_POD_RESTARTS}, |
| 1505 | + expected_output=get_train_job_with_restarted_pods_data_type( |
| 1506 | + runtime_name=TORCH_RUNTIME, |
| 1507 | + train_job_name=JOB_WITH_POD_RESTARTS, |
| 1508 | + ), |
| 1509 | + ), |
1450 | 1510 | TestCase( |
1451 | 1511 | name="timeout error when getting job", |
1452 | 1512 | expected_status=FAILED, |
|
0 commit comments