diff --git a/pyproject.toml b/pyproject.toml index 0014e480..ce66d237 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ classifiers = [ # Applications that consume this library should be the ones that are more strictly # limiting dependencies if they want/need to. dependencies = [ - "boto3 >= 1.34.75", + "boto3 >= 1.36.8", "click >= 8.1.7", "pyyaml >= 6.0", # Job Attachments diff --git a/src/deadline/client/api/_submit_job_bundle.py b/src/deadline/client/api/_submit_job_bundle.py index 3b0c32bf..43cb2f45 100644 --- a/src/deadline/client/api/_submit_job_bundle.py +++ b/src/deadline/client/api/_submit_job_bundle.py @@ -60,6 +60,7 @@ def create_job_from_job_bundle( priority: Optional[int] = None, max_failed_tasks_count: Optional[int] = None, max_retries_per_task: Optional[int] = None, + max_worker_count: Optional[int] = None, print_function_callback: Callable[[str], None] = lambda msg: None, decide_cancel_submission_callback: Callable[ [AssetUploadGroup], bool @@ -122,6 +123,7 @@ def create_job_from_job_bundle( config (ConfigParser, optional): The AWS Deadline Cloud configuration object to use instead of the config file. priority (int, optional): explicit value for the priority of the job. + max_worker_count (int, optional): explicit value for the max worker count of the job. max_failed_tasks_count (int, optional): explicit value for the maximum allowed failed tasks. max_retries_per_task (int, optional): explicit value for the maximum retries per task. print_function_callback (Callable str -> None, optional): Callback to print messages produced in this function. @@ -309,6 +311,8 @@ def create_job_from_job_bundle( if priority is not None: create_job_args["priority"] = priority + if max_worker_count is not None: + create_job_args["maxWorkerCount"] = max_worker_count if max_failed_tasks_count is not None: create_job_args["maxFailedTasksCount"] = max_failed_tasks_count if max_retries_per_task is not None: diff --git a/src/deadline/client/cli/_groups/bundle_group.py b/src/deadline/client/cli/_groups/bundle_group.py index c753a961..c9709811 100644 --- a/src/deadline/client/cli/_groups/bundle_group.py +++ b/src/deadline/client/cli/_groups/bundle_group.py @@ -98,6 +98,11 @@ def validate_parameters(ctx, param, value): type=int, help="The maximum number of times to retry a task before it is marked as failed.", ) +@click.option( + "--max-worker-count", + type=int, + help="The max worker count of the job.", +) @click.option( "--job-attachments-file-system", help="The method workers use to access job attachments. " @@ -132,6 +137,7 @@ def bundle_submit( priority, max_failed_tasks_count, max_retries_per_task, + max_worker_count, require_paths_exist, submitter_name, **args, @@ -208,6 +214,7 @@ def _decide_cancel_submission(upload_group: AssetUploadGroup) -> bool: priority=priority, max_failed_tasks_count=max_failed_tasks_count, max_retries_per_task=max_retries_per_task, + max_worker_count=max_worker_count, hashing_progress_callback=hash_callback_manager.callback, upload_progress_callback=upload_callback_manager.callback, create_job_result_callback=_check_create_job_wait_canceled, diff --git a/src/deadline/client/job_bundle/submission.py b/src/deadline/client/job_bundle/submission.py index 1b66092d..e754dd0f 100644 --- a/src/deadline/client/job_bundle/submission.py +++ b/src/deadline/client/job_bundle/submission.py @@ -20,6 +20,7 @@ "priority", "maxFailedTasksCount", "maxRetriesPerTask", + "maxWorkerCount", ] diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index 5a120133..606402f1 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -17,6 +17,7 @@ QHBoxLayout, QLabel, QLineEdit, + QRadioButton, QSpinBox, QVBoxLayout, QWidget, @@ -291,6 +292,27 @@ def _build_ui(self): self.max_retries_per_task_box.setRange(0, 2147483647) self.layout.addRow(self.max_retries_per_task_box_label, self.max_retries_per_task_box) + self.max_worker_count_box_label = QLabel("Maximum worker count") + self.max_worker_count_box_label.setToolTip("Maximum worker count of job.") + self.max_worker_count_box = QSpinBox() + self.max_worker_count_box.setRange(1, 2147483647) + self.unlimited_max_worker_count = QRadioButton("No max worker count") + self.limited_max_worker_count = QRadioButton("Set max worker count") + self.limited_max_worker_count.toggled.connect( + self.limited_max_worker_count_radio_button_toggled + ) + self.max_worker_count_layout = QVBoxLayout(self) + self.max_worker_count_layout.addWidget(self.unlimited_max_worker_count) + self.max_worker_count_layout.addWidget(self.limited_max_worker_count) + self.max_worker_count_layout.addWidget(self.max_worker_count_box) + self.layout.addRow(self.max_worker_count_box_label, self.max_worker_count_layout) + + def limited_max_worker_count_radio_button_toggled(self, state): + """ + Enable the max worker count text box when limited max worker count radio button is enabled. + """ + self.max_worker_count_box.setHidden(not state) + def refresh_ui(self, settings: Any): self.sub_name_edit.setText(settings.name) self.desc_edit.setText(settings.description) @@ -298,6 +320,9 @@ def refresh_ui(self, settings: Any): self.max_failed_tasks_count_box.setValue(20) self.max_retries_per_task_box.setValue(5) self.priority_box.setValue(50) + self.unlimited_max_worker_count.setChecked(True) + self.limited_max_worker_count.setChecked(False) + self.max_worker_count_box.setHidden(True) def set_parameter_value(self, parameter: dict[str, Any]): """ @@ -315,6 +340,16 @@ def set_parameter_value(self, parameter: dict[str, Any]): self.max_retries_per_task_box.setValue(parameter["value"]) elif parameter_name == "deadline:priority": self.priority_box.setValue(parameter["value"]) + elif parameter_name == "deadline:maxWorkerCount": + if parameter["value"] == -1: + self.unlimited_max_worker_count.setChecked(True) + self.limited_max_worker_count.setChecked(False) + self.max_worker_count_box.setHidden(True) + else: + self.unlimited_max_worker_count.setChecked(False) + self.limited_max_worker_count.setChecked(True) + self.max_worker_count_box.setHidden(False) + self.max_worker_count_box.setValue(parameter["value"]) else: raise KeyError(parameter_name) @@ -323,7 +358,7 @@ def get_parameters(self): Returns a list of OpenJD parameter definition dicts with a "value" key filled from the widget. """ - return [ + job_parameters = [ { "name": "deadline:targetTaskRunStatus", "type": "STRING", @@ -358,6 +393,15 @@ def get_parameters(self): }, {"name": "deadline:priority", "type": "INT", "value": self.priority_box.value()}, ] + if not self.unlimited_max_worker_count.isChecked(): + job_parameters.append( + { + "name": "deadline:maxWorkerCount", + "type": "INT", + "value": self.max_worker_count_box.value(), + } + ) + return job_parameters def update_settings(self, settings): """ diff --git a/test/unit/deadline_client/api/test_job_bundle_submission.py b/test/unit/deadline_client/api/test_job_bundle_submission.py index c735dad6..e591fcb4 100644 --- a/test/unit/deadline_client/api/test_job_bundle_submission.py +++ b/test/unit/deadline_client/api/test_job_bundle_submission.py @@ -204,6 +204,10 @@ def get_minimal_json_job_template(job_name): { "name": "deadline:maxRetriesPerTask", "value": 5 + }, + { + "name": "deadline:maxWorkerCount", + "value": 10 } ] } @@ -213,6 +217,7 @@ def get_minimal_json_job_template(job_name): "targetTaskRunStatus": "SUSPENDED", "maxFailedTasksCount": 20, "maxRetriesPerTask": 5, + "maxWorkerCount": 10, }, ), "DEADLINE_ONLY_YAML": ( @@ -227,12 +232,15 @@ def get_minimal_json_job_template(job_name): value: 250 - name: "deadline:maxRetriesPerTask" value: 15 +- name: "deadline:maxWorkerCount" + value: 10 """, { "priority": 45, "targetTaskRunStatus": "SUSPENDED", "maxFailedTasksCount": 250, "maxRetriesPerTask": 15, + "maxWorkerCount": 10, }, ), # A parameter_values.json/yaml file with just job template values diff --git a/test/unit/deadline_client/cli/test_cli_bundle.py b/test/unit/deadline_client/cli/test_cli_bundle.py index 97ff8490..da7ba06e 100644 --- a/test/unit/deadline_client/cli/test_cli_bundle.py +++ b/test/unit/deadline_client/cli/test_cli_bundle.py @@ -201,7 +201,7 @@ def test_cli_bundle_explicit_parameters(fresh_deadline_config): def test_cli_bundle_priority_retries(fresh_deadline_config): """ - Confirm that --priority, --max-failed-tasks-count, and --max-retries-per-task get passed in from the CLI. + Confirm that --priority, --max-failed-tasks-count, --max_worker_count and --max-retries-per-task get passed in from the CLI. """ # Use a temporary directory for the job bundle with tempfile.TemporaryDirectory() as tmpdir, patch.object(boto3, "Session") as session_mock: @@ -230,6 +230,8 @@ def test_cli_bundle_priority_retries(fresh_deadline_config): "12", "--max-retries-per-task", "4", + "--max-worker-count", + "123", ], ) @@ -244,6 +246,7 @@ def test_cli_bundle_priority_retries(fresh_deadline_config): priority=25, maxFailedTasksCount=12, maxRetriesPerTask=4, + maxWorkerCount=123, ) assert result.exit_code == 0 diff --git a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py index 968b4d4d..c73caa7f 100644 --- a/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py +++ b/test/unit/deadline_client/ui/widgets/test_shared_job_settings_tab.py @@ -73,3 +73,10 @@ def test_max_retries_per_task_should_be_integer_within_range( ): shared_job_settings_tab.shared_job_properties_box.max_retries_per_task_box.setValue(-1) assert shared_job_settings_tab.shared_job_properties_box.max_retries_per_task_box.value() == 0 + + +def test_max_worker_count_should_be_integer_within_range( + shared_job_settings_tab: SharedJobSettingsWidget, +): + shared_job_settings_tab.shared_job_properties_box.max_worker_count_box.setValue(-1) + assert shared_job_settings_tab.shared_job_properties_box.max_worker_count_box.value() == 1