Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c28564c
Improve error message for duplicate pipeline run names
strickvl May 24, 2025
0b903f7
Add more specific examples to run_name documentation
strickvl May 24, 2025
659288c
Fix mypy type errors for IntegrityError import
strickvl May 24, 2025
3d2a22d
Address PR review comments for duplicate run name handling
strickvl May 24, 2025
22bc668
Merge remote-tracking branch 'origin/develop' into feature/better-err…
strickvl May 26, 2025
23bced4
Move duplicate run name error handling to SQLZenStore
strickvl May 26, 2025
6dc98fe
Fix docstring linter error in create_placeholder_run
strickvl May 26, 2025
508a616
Merge branch 'develop' into feature/better-error-message
htahir1 May 26, 2025
86307b4
Merge branch 'develop' into feature/better-error-message
htahir1 Jun 24, 2025
0e54848
Replace mocked test with integration test for duplicate pipeline runs
strickvl Jun 25, 2025
816b870
Merge branch 'develop' into feature/better-error-message
strickvl Jun 25, 2025
b86a000
Fix SQLAlchemy autoflush issue in pipeline run creation
strickvl Jun 25, 2025
e86c84a
Clean up outdated comments in integration test
strickvl Jun 25, 2025
419a8e8
Merge branch 'develop' into feature/better-error-message
strickvl Jul 3, 2025
4ed0930
Refactor duplicate pipeline run name error handling
strickvl Jul 3, 2025
af7ae3c
Merge branch 'develop' into feature/better-error-message
strickvl Jul 3, 2025
4bd95f2
Extract duplicate error message logic into helper method
strickvl Jul 3, 2025
7b05e07
Update src/zenml/pipelines/run_utils.py
strickvl Jul 3, 2025
c15f153
Merge remote-tracking branch 'origin/develop' into feature/better-err…
strickvl Jul 29, 2025
7bfe723
Merge branch 'develop' into feature/better-error-message
strickvl Jul 31, 2025
03700a9
Merge branch 'develop' into feature/better-error-message
strickvl Sep 3, 2025
8107860
Merge branch 'develop' into feature/better-error-message
strickvl Sep 23, 2025
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
19 changes: 19 additions & 0 deletions docs/book/how-to/steps-pipelines/yaml_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,25 @@ Set a custom name for the pipeline run:
run_name: "training_run_cifar10_resnet50_lr0.001"
```

{% hint style="warning" %}
**Important:** Pipeline run names must be unique within a project. If you try to run a pipeline with a name that already exists, you'll get an error. To avoid this:

1. **Use dynamic placeholders** to ensure uniqueness:
```yaml
# Example 1: Use placeholders for date and time to ensure uniqueness
run_name: "training_run_{date}_{time}"

# Example 2: Combine placeholders with specific details for better context
run_name: "training_run_cifar10_resnet50_lr0.001_{date}_{time}"
```

2. **Remove the 'run_name' from your config** to let ZenML auto-generate unique names

3. **Change the run_name** before rerunning the pipeline

Available placeholders: `{date}`, `{time}`, and any parameters defined in your pipeline configuration.
{% endhint %}

## Resource and Component Configuration

### Docker Settings
Expand Down
60 changes: 58 additions & 2 deletions src/zenml/pipelines/run_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from zenml.config.source import Source, SourceType
from zenml.config.step_configurations import StepConfigurationUpdate
from zenml.enums import ExecutionStatus
from zenml.exceptions import EntityExistsError
from zenml.logger import get_logger
from zenml.models import (
FlavorFilter,
Expand All @@ -28,6 +29,14 @@
from zenml.utils.time_utils import utc_now
from zenml.zen_stores.base_zen_store import BaseZenStore

if TYPE_CHECKING:
from sqlalchemy.exc import IntegrityError as SQLIntegrityError
else:
try:
from sqlalchemy.exc import IntegrityError as SQLIntegrityError
except ImportError:
SQLIntegrityError = None

if TYPE_CHECKING:
StepConfigurationUpdateOrDict = Union[
Dict[str, Any], StepConfigurationUpdate
Expand Down Expand Up @@ -63,6 +72,10 @@ def create_placeholder_run(

Returns:
The placeholder run or `None` if no run was created.

Raises:
EntityExistsError: If a pipeline run with the same name already exists,
with an improved error message suggesting solutions.
"""
assert deployment.user

Expand Down Expand Up @@ -91,8 +104,51 @@ def create_placeholder_run(
tags=deployment.pipeline_configuration.tags,
logs=logs,
)
run, _ = Client().zen_store.get_or_create_run(run_request)
return run

try:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • This should be handled in the SQLZenStore, not in this random place (which is only one occurence where a run is created).
  • Specifying the run name in a config file is not the only one way to do it, the message can simply be generic and talk about configuration instead of files.

run, _ = Client().zen_store.get_or_create_run(run_request)
return run
except (EntityExistsError, RuntimeError) as e:
# Handle both EntityExistsError and raw database IntegrityError
original_message = str(e)

# Check for duplicate run name patterns in the error message
is_duplicate_run_name = False
run_name = run_request.name

# Check for ZenML's EntityExistsError
if isinstance(e, EntityExistsError) and (
"pipeline run" in original_message.lower()
and "name" in original_message.lower()
):
is_duplicate_run_name = True

# Check for raw SQL IntegrityError
elif (
SQLIntegrityError is not None
and (isinstance(e.__cause__ or e, SQLIntegrityError))
or "unique_run_name_in_project" in original_message
or (
"duplicate entry" in original_message.lower()
and run_name in original_message
)
):
is_duplicate_run_name = True

if is_duplicate_run_name:
improved_message = (
f"Pipeline run name '{run_name}' already exists in this project. "
f"Each pipeline run must have a unique name.\n\n"
f"To fix this, you can:\n"
f"1. Change the 'run_name' in your config file to a unique value\n"
f'2. Use a dynamic run name with placeholders like: run_name: "{run_name}_{{date}}_{{time}}"\n'
f"3. Remove the 'run_name' from your config to auto-generate unique names\n\n"
f"For more information on run naming, see: https://docs.zenml.io/concepts/steps_and_pipelines/yaml_configuration#run-name"
)
raise EntityExistsError(improved_message) from e

# Re-raise the original error if it's not about duplicate run names
raise


def get_placeholder_run(
Expand Down
130 changes: 130 additions & 0 deletions tests/unit/pipelines/test_run_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied. See the License for the specific language governing
# permissions and limitations under the License.
from unittest.mock import MagicMock, patch
from uuid import uuid4

import pytest

from zenml.exceptions import EntityExistsError
from zenml.models import (
PipelineDeploymentResponse,
UserResponse,
)
from zenml.pipelines import run_utils


Expand All @@ -20,3 +30,123 @@ def test_default_run_name():
run_utils.get_default_run_name(pipeline_name="my_pipeline")
== "my_pipeline-{date}-{time}"
)


@patch("zenml.pipelines.run_utils.Client")
def test_create_placeholder_run_duplicate_name_error(mock_client):
"""Test that create_placeholder_run provides a helpful error message for duplicate run names."""
# Mock the deployment
deployment = MagicMock(spec=PipelineDeploymentResponse)
deployment.user = MagicMock(spec=UserResponse)
deployment.schedule = None
deployment.run_name_template = "my_test_run"
deployment.pipeline_configuration.finalize_substitutions.return_value = {}
deployment.project.id = uuid4()
deployment.id = uuid4()
deployment.pipeline.id = uuid4()
deployment.pipeline_configuration.tags = []

# Mock the client and zen_store to raise EntityExistsError
original_error_message = (
"Unable to create the requested pipeline run with name 'my_test_run': "
"Found another existing pipeline run with the same name in the 'test_project' project."
)
mock_client.return_value.zen_store.get_or_create_run.side_effect = (
EntityExistsError(original_error_message)
)

# Test that our improved error message is raised
with pytest.raises(EntityExistsError) as exc_info:
run_utils.create_placeholder_run(deployment)

error_message = str(exc_info.value)

# Verify the improved error message contains helpful guidance
assert (
"Pipeline run name 'my_test_run' already exists in this project"
in error_message
)
assert "Each pipeline run must have a unique name" in error_message
assert "Change the 'run_name' in your config" in error_message
assert "Use a dynamic run name with placeholders" in error_message
assert "Remove the 'run_name' from your config" in error_message
assert (
"https://docs.zenml.io/concepts/steps_and_pipelines/yaml_configuration#run-name"
in error_message
)


@patch("zenml.pipelines.run_utils.Client")
def test_create_placeholder_run_duplicate_name_runtime_error(mock_client):
"""Test that create_placeholder_run handles RuntimeError with IntegrityError for duplicate run names."""
# Mock the deployment
deployment = MagicMock(spec=PipelineDeploymentResponse)
deployment.user = MagicMock(spec=UserResponse)
deployment.schedule = None
deployment.run_name_template = "my_test_run"
deployment.pipeline_configuration.finalize_substitutions.return_value = {}
deployment.project.id = uuid4()
deployment.id = uuid4()
deployment.pipeline.id = uuid4()
deployment.pipeline_configuration.tags = []

# Mock the client and zen_store to raise RuntimeError with IntegrityError message
# This simulates what happens in the REST zen store
original_error_message = (
'(pymysql.err.IntegrityError) (1062, "Duplicate entry '
"'my_test_run-6e23c0466cc4411c8b9f75f0c8a1a818' for key "
"'pipeline_run.unique_run_name_in_project'\")"
)
mock_client.return_value.zen_store.get_or_create_run.side_effect = (
RuntimeError(original_error_message)
)

# Test that our improved error message is raised
with pytest.raises(EntityExistsError) as exc_info:
run_utils.create_placeholder_run(deployment)

error_message = str(exc_info.value)

# Verify the improved error message contains helpful guidance
assert (
"Pipeline run name 'my_test_run' already exists in this project"
in error_message
)
assert "Each pipeline run must have a unique name" in error_message
assert "Change the 'run_name' in your config file" in error_message
assert "Use a dynamic run name with placeholders" in error_message
assert "Remove the 'run_name' from your config" in error_message
assert (
"https://docs.zenml.io/concepts/steps_and_pipelines/yaml_configuration#run-name"
in error_message
)


@patch("zenml.pipelines.run_utils.Client")
def test_create_placeholder_run_non_duplicate_name_error(mock_client):
"""Test that create_placeholder_run re-raises non-duplicate-name EntityExistsErrors unchanged."""
# Mock the deployment
deployment = MagicMock(spec=PipelineDeploymentResponse)
deployment.user = MagicMock(spec=UserResponse)
deployment.schedule = None
deployment.run_name_template = "my_test_run"
deployment.pipeline_configuration.finalize_substitutions.return_value = {}
deployment.project.id = uuid4()
deployment.id = uuid4()
deployment.pipeline.id = uuid4()
deployment.pipeline_configuration.tags = []

# Mock the client and zen_store to raise a different EntityExistsError
original_error_message = "Some other entity exists error"
mock_client.return_value.zen_store.get_or_create_run.side_effect = (
EntityExistsError(original_error_message)
)

# Test that the original error message is preserved for non-duplicate-name errors
with pytest.raises(EntityExistsError) as exc_info:
run_utils.create_placeholder_run(deployment)

error_message = str(exc_info.value)

# Verify the original error message is preserved
assert error_message == original_error_message
Loading