Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion requirements.conda.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ sqlalchemy>=2.0.39,<3
stomp.py>=7,<9
workflows>=3.5
xmltodict
zocalo>=1.4.0
zocalo>=1.5.0
14 changes: 8 additions & 6 deletions src/dlstbx/services/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,16 +110,16 @@ def submit_to_slurm(
"array": params.array,
}
if params.min_memory_per_cpu:
jdm_params["memory_per_cpu"] = slurm.models.Uint64NoVal(
jdm_params["memory_per_cpu"] = slurm.models.Uint64NoValStruct(
number=params.min_memory_per_cpu, set=True
)
if params.memory_per_node:
jdm_params["memory_per_node"] = slurm.models.Uint64NoVal(
jdm_params["memory_per_node"] = slurm.models.Uint64NoValStruct(
number=params.memory_per_node, set=True
)
if params.time_limit:
time_limit_minutes = math.ceil(params.time_limit.total_seconds() / 60)
jdm_params["time_limit"] = slurm.models.Uint32NoVal(
jdm_params["time_limit"] = slurm.models.Uint32NoValStruct(
number=time_limit_minutes, set=True
)
if params.gpus_per_node:
Expand All @@ -135,9 +135,11 @@ def submit_to_slurm(
except requests.HTTPError as e:
logger.error(f"Failed Slurm job submission: {e}\n{e.response.text}")
return None
if response.error:
error_message = f"{response.error_code}: {response.error}"
logger.error(f"Failed Slurm job submission: {error_message}")
if response.errors and response.errors.root:
error_messages = []
for error in response.errors.root:
error_messages.append(f"{error.error_number}: {error.error}")
logger.error(f"Failed Slurm job submission: {'; '.join(error_messages)}")
return None
return response.job_id

Expand Down
138 changes: 138 additions & 0 deletions tests/services/test_cluster.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""Unit tests for the cluster service submit_to_slurm function."""

from __future__ import annotations

import logging
from unittest import mock

import pytest
import requests
from zocalo.util import slurm

from dlstbx.services.cluster import JobSubmissionParameters, submit_to_slurm


@pytest.fixture
def mock_slurm_api():
with mock.patch(
"dlstbx.services.cluster.slurm.SlurmRestApi.from_zocalo_configuration"
) as mock_api_class:
api = mock.Mock()
api.user_name = "gda2"
api.submit_job = mock.Mock()
mock_api_class.return_value = api
yield api


@pytest.fixture
def mock_successful_submission_response():
response = mock.Mock()
response.job_id = 12345
response.errors = None
return response


@pytest.fixture
def mock_error_response():
error1 = slurm.models.OpenapiError(error_number=1, error="Invalid request")
error2 = slurm.models.OpenapiError(error_number=2, error="Permission denied")

response = slurm.models.OpenapiJobSubmitResponse(
errors=slurm.models.OpenapiErrors(root=[error1, error2])
)
return response


@pytest.fixture
def logger():
return logging.getLogger("test_logger")


@pytest.fixture
def mock_config():
config = mock.Mock()
config.active_environments = ["live"]
return config


@pytest.fixture
def temp_working_dir(tmp_path):
return tmp_path / "work"


def test_submit_to_slurm_success(
mock_slurm_api,
mock_successful_submission_response,
logger,
mock_config,
temp_working_dir,
):
mock_slurm_api.submit_job.return_value = mock_successful_submission_response

params = JobSubmissionParameters(commands="echo 'Hello'", partition="cs04r")

job_id = submit_to_slurm(
params,
temp_working_dir,
logger,
mock_config,
scheduler="slurm",
recipewrapper="/tmp/recipe.json",
)

assert job_id == 12345
assert mock_slurm_api.submit_job.call_count == 1

job_submission = mock_slurm_api.submit_job.call_args[0][0]
assert isinstance(job_submission, slurm.models.JobSubmitReq)
assert job_submission.job.partition == "cs04r"
assert job_submission.script.startswith("#!/bin/bash")


def test_submit_to_slurm_response_errors(
mock_slurm_api,
mock_error_response,
logger,
mock_config,
temp_working_dir,
):
mock_slurm_api.submit_job.return_value = mock_error_response

params = JobSubmissionParameters(commands="echo 'Hello'", partition="cs04r")

job_id = submit_to_slurm(
params,
temp_working_dir,
logger,
mock_config,
scheduler="slurm",
recipewrapper="/tmp/recipe.json",
)

assert job_id is None


def test_submit_to_slurm_http_error(
mock_slurm_api,
logger,
mock_config,
temp_working_dir,
):
error_response = mock.Mock()
error_response.text = "Internal Server Error"
http_error = requests.HTTPError("500 Server Error")
http_error.response = error_response
mock_slurm_api.submit_job.side_effect = http_error

params = JobSubmissionParameters(commands="echo 'Hello'", partition="cs04r")

job_id = submit_to_slurm(
params,
temp_working_dir,
logger,
mock_config,
scheduler="slurm",
recipewrapper="/tmp/recipe.json",
)

assert job_id is None